The Python unittest module requires a lot of boilerplate code to set up and initialize tests. It is based on the very popular JUnit testing framework for Java. It even uses the same method names (you may have noticed they don't conform to the PEP-8 naming standard, which suggests snake_case rather than CamelCase to indicate a method name) and test layout. While this is effective for testing in Java, it's not necessarily the best design for Python testing. I actually find the unittest framework to be an excellent example of overusing object-oriented principles.
Because Python programmers like their code to be elegant and simple, other test frameworks have been developed, outside the standard library. Two of the more popular ones are pytest and nose. The former is more robust and has had Python 3 support for much longer, so we'll discuss it here.
Since pytest is not part of the standard library, you'll need to download and install it yourself. You can get it from the pytest home page at http://pytest.org/. The website has comprehensive installation instructions for a variety of interpreters and platforms, but you can usually get away with the more common Python package installer, pip. Just type pip install pytest on your command line and you'll be good to go.
pytest has a substantially different layout from the unittest module. It doesn't require test cases to be classes. Instead, it takes advantage of the fact that Python functions are objects, and allows any properly named function to behave like a test. Rather than providing a bunch of custom methods for asserting equality, it uses the assert statement to verify results. This makes tests more readable and maintainable.
When we run pytest, it starts in the current folder and searches for any modules or subpackages with names beginning with the characters test_. If any functions in this module also start with test, they will be executed as individual tests. Furthermore, if there are any classes in the module whose name starts with Test, any methods on that class that start with test_ will also be executed in the test environment.
Using the following code, let's port the simplest possible unittest example we wrote earlier to pytest:
def test_int_float(): assert 1 == 1.0
For the exact same test, we've written two lines of more readable code, in comparison to the six lines required in our first unittest example.
However, we are not forbidden from writing class-based tests. Classes can be useful for grouping related tests together or for tests that need to access related attributes or methods on the class. The following example shows an extended class with a passing and a failing test; we'll see that the error output is more comprehensive than that provided by the unittest module:
class TestNumbers: def test_int_float(self): assert 1 == 1.0 def test_int_str(self): assert 1 == "1"
Notice that the class doesn't have to extend any special objects to be picked up as a test (although pytest will run standard unittest TestCases just fine). If we run pytest <filename>, the output looks as follows:
============================== test session starts ==============================
platform linux -- Python 3.7.0, pytest-3.8.0, py-1.6.0, pluggy-0.7.1
rootdir: /home/dusty/Py3OOP/Chapter 12: Testing Object-oriented Programs, inifile:
collected 3 items
test_with_pytest.py ..F [100%]
=================================== FAILURES ====================================
___________________________ TestNumbers.test_int_str ____________________________
self = <test_with_pytest.TestNumbers object at 0x7fdb95e31390>
def test_int_str(self):
> assert 1 == "1"
E AssertionError: assert 1 == '1'
test_with_pytest.py:10: AssertionError
====================== 1 failed, 2 passed in 0.03 seconds =======================
The output starts with some useful information about the platform and interpreter. This can be useful for sharing or discussing bugs across disparate systems. The third line tells us the name of the file being tested (if there are multiple test modules picked up, they will all be displayed), followed by the familiar .F we saw in the unittest module; the . character indicates a passing test, while the letter F demonstrates a failure.
After all tests have run, the error output for each of them is displayed. It presents a summary of local variables (there is only one in this example: the self parameter passed into the function), the source code where the error occurred, and a summary of the error message. In addition, if an exception other than an AssertionError is raised, pytest will present us with a complete traceback, including source code references.
By default, pytest suppresses output from print statements if the test is successful. This is useful for test debugging; when a test is failing, we can add print statements to the test to check the values of specific variables and attributes as the test runs. If the test fails, these values are output to help with diagnosis. However, once the test is successful, the print statement output is not displayed, and they are easily ignored. We don't have to clean up output by removing print statements. If the tests ever fail again, due to future changes, the debugging output will be immediately available.