pytest
Overview
While not part of the Python standard library like unittest, pytest is in many ways the faster, simpler, and user-friendly alternative to unittest. unittest requires a lot of boilerplate code to set a test suite up, including a module, class, and test methods inside said class. There is a significantly larger learning curve and more concepts to understand in order to properly utilize unittest.
pytest on the other hand, is extremely straightforward to use, and there are very few concepts that need to be understood in order to take advantage.
Let’s say we have a function that accepts n digits and returns the product of all of the numbers.
import functools
def product(*numbers: float) -> float:
return functools.reduce(lambda x, y: x*y, numbers)
Now, using unittest, we would first need to create a module as follows.
import unittest
import mymodule
class TestMyModule(unittest.TestCase):
def test_product(self):
result = mymodule.product(1, 2, 3)
self.assertEqual(result, 6)
if __name__ == '__main__':
unittest.main()
To use the test_mymodule.py module to actually test mymodule.py, we would need to run the following command.
python -m unittest test_mymodule
. ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
To better visualize the difference, the equivalent functionality is much more straightforward using pytest.
import mymodule
def test_product():
result = mymodule.product(1, 2, 3)
assert result==6
Then, to run the tests.
python -m pytest
=========================================== test session starts =========================================== platform darwin -- Python 3.9.2, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 rootdir: /Users/kamstut plugins: anyio-2.2.0 collected 1 item test_mymodule.py . [100%] ============================================ 1 passed in 0.01s ============================================
As you can see, pytest involves less boilerplate code. pytest is easier to use and run. The results of pytest are easier to read, and pytest is faster.
|
By default |
Parametrizing tests
Parametrizing tests is a really good way to test a variety of inputs, all at once. For example, let’s say we have a function called my_func that accepts a value, $v$ and returns $100/v$. Let’s say we were provided the given function, with some doctests.
def my_func(v: float) -> float:
"""
Given a value, $v$, return 100/v.
>>> my_func(-2.0)
-50.0
>>> my_func(2.0)
50.0
>>> my_func(0.5)
200.0
"""
return 100/v
The doctests pass with flying colors. With that being said, we are working with computers — it doesn’t make sense to just test 3 human-picked values does it? Especially considering we don’t want to type out every single test we want to run.
This is where parametrizing tests come in. We can create a test that runs the function with a whole range of inputs, which could, in theory, help us test more thoroughly. The following pytest module, allows us to do this. Rather than test for a specific value, we can just test to make sure that the type of the result is a float.
import pytest
import mymodule
import numpy as np
@pytest.mark.parametrize('v', [float(n) for n in np.arange(-100.0, 100.0, 1)])
def test_my_func(v: float):
assert isinstance(mymodule.my_func(v), float)
python -m pytest
...
================================================ FAILURES ================================================
___________________________________________ test_my_func[0.0] ____________________________________________
v = 0.0
@pytest.mark.parametrize('v', [float(n) for n in np.arange(-100.0, 100.0, 1)])
def test_my_func(v: float):
> assert isinstance(mymodule.my_func(v), float)
test_mymodule.py:7:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
v = 0.0
def my_func(v: float) -> float:
"""
Given a value, $v$, return 100/v.
>>> my_func(-2.0)
-50.0
>>> my_func(2.0)
50.0
>>> my_func(0.5)
200.0
"""
> return 100/v
E ZeroDivisionError: float division by zero
mymodule.py:14: ZeroDivisionError
======================================== short test summary info =========================================
FAILED test_mymodule.py::test_my_func[0.0] - ZeroDivisionError: float division by zero
===================================== 1 failed, 199 passed in 0.43s ======================================
Ah ha! Whoever wrote this code didn’t consider the case when the value is 0.0.
Resources
An excellent an extensive guide to using pytest to test your Python code from realpython.com.