Unit Testing With pytest
Learn how to efficiently create and manage unit and integration tests using pytest for streamlined and expressive testing.
Overview
We can create unit tests using a library that provides a common framework for the test scenarios, along with a test runner to execute the tests and log results. Unit tests focus on testing the least amount of code possible in any one test. The standard library includes the unittest
package. While widely used, this package tends to force us to create a fair amount of boilerplate code for each test case.
One of the more popular alternatives to the standard library unittest
is pytest
. This has the advantage of letting us write smaller, and more clear, test cases. The lack of overheads makes this a desirable alternative.
The distinctive features of pytest
The pytest
tool can use a substantially different test layout from the unittest
module. It doesn’t require test cases to be subclasses of unittest.TestCase
. Instead, it takes advantage of the fact that Python functions are first-class 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 simpler, more readable, and, consequently, easier to maintain.
When we run pytest
, it starts in the current folder and searches for any modules or sub-packages with names beginning with the characters test_.
(Including the _
character.) If any functions in this module also start with test (no _
required), 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.
It also searches in a folder named—unsurprisingly—tests
. Because of this, it’s common to have code broken up into two folders: the src/
directory contains the working module, library, or application, while the tests/
directory contains all the test cases.
Using the following code, let’s port the simple unittest
example we wrote earlier to pytest
:
def test_int_float() -> None:assert 1 == 1.0
For the 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) -> None:assert 1 == 1.0def test_int_str(self) -> None:assert 1 == "1"
Note: The
pytest
module is already set up for you in the playground.
Decoding rest results: Pass and failure
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()
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()
output is not displayed, and they are easily ignored. We don’t have to clean up the test output by removing print()
. If the tests ever fail again, due to future changes, the debugging output will be immediately available.
pytest
handling GIVEN
precondition
Interestingly, this use of the assert
statement exposes a potential problem to mypy. When we use the assert statement, mypy can examine the types, and will alert us to a potential problem with assert 1 == "1"
. This code is unlikely to be right, and it will not only ...