Python in the Enterprise Part 3: Unit testing with pytest
The unittest
module, in the Python standard library, is based on JUnit (a Java library) and thus doesn’t feel very Pythonic. Pytest turns this upside down and provides a more Python developer friendly experience for writing unit tests.
Installation
$ pip install pytest
Basic test
Unlike the unittest
module which defines multiple assertXXX methods for writing assertions, pytest simply uses Python’s built-in assert
statement.
# tests/test_hello.py
def test():
assert "abc" == "def"
Running from command line
$ pytest -v tests/
Pytest is still able to show detailed information about why a test case failed even though it uses the built in assert
statements:
_____________________________ test_abc _____________________________
def test_abc():
> assert "abc" == "def"
E AssertionError: assert 'abc' == 'def'
E - def
E + abc
tests/test_hello.py:3: AssertionError
===================== short test summary info ======================
FAILED tests/test_hello.py::test_abc - AssertionError: assert 'ab...
Asserting that exceptions are raised
import pytest
class GreetingException(Exception):
def __init__(self, name):
self.name = name
def test_ex():
with pytest.raises(GreetingException) as exc_info:
raise(GreetingException("bob"))
# Exception is captured in exc_info.value
assert exc_info.value.name == "bob"
Fixtures and arguments
import pytest
from unittest.mock import Mock
@pytest.fixture
def greeting_service():
return Mock()
# test case that uses the greeting_service fixture
def test_greeting(greeting_service):
greeting_service.greet()
Sharing fixtures among test modules
Move the greeting_service
fixture to a file name conftest.py
:
# conftest.py
import pytest
from unittest.mock import Mock
@pytest.fixture
def greeting_service():
return Mock()
# test_greeting2.py
def test_greeting2(greeting_service):
greeting_service.greet()
The fixture function will be invoked for each test method that depends on it - this is called “function” scope. If setting up the fixture is expensive a different scope can be assigned to a text fixture. The supported scopes are - function (the default), class, module, package or session.
import pytest
@pytest.fixture(scope="module")
def greeting_service():
pass
module
scope here means that all the test cases in a Python module will share the same instance of the fixture.
Cleaning up fixtures
import pytest
from unittest.mock import Mock
@pytest.fixture
def greeting_service():
service = Service()
yield service
# Clean up code for service
service.close()
Disabling stdout capture
By default, pytest will suppress any messages that test cases print to stdout/stderr. To disable this behavior use the -s
option.
$ pytest -s -v tests/
async test functions
To test async functions, install pytest-asyncio:
$ pip install pytest-asyncio
Then decorate your test functions with @pytest.mark.asyncio
:
import pytest
import asyncio
@pytest.mark.asyncio
async def test_async():
await asyncio.sleep(5)
Running a test case multiple times
Sometimes a piece of logic works if invoked once but fails if invoked multiple times. For this, we can instruct pytest to run a test case multiple times using the pytest-repeat
package.
$ pip install pytest-repeat
With the command line we can use the --count
argument to execute the tests multiple times:
$ pytest --count=50 -v tests/test_greeting.py
In code, we can use the decorator @pytest.mark.repeat
to specify the number of times to repeat a test case:
import pytest
@pytest.mark.repeat(5)
def test_greeting():
pass