Pytest
this document was inspired/copied from of https://docs.python.org/3/library/unittest.html, https://docs.pytest.org and https://pytest-django.readthedocs.io/en/latest/
Pytest is a Python testing framework that originated from the PyPy project. It can be used to write various types of software tests, including unit tests, integration tests, end-to-end tests, and functional tests. Its features include parametrized testing, fixtures, and assert re-writing.
Python doesn't include Pytest out of the box, you must install it. Please check https://docs.pytest.org
Anatomy of a test
Pytest divides a test into four steps:
- Arrange is where we prepare everything for our test. This means pretty much everything except for the “act”. This can mean preparing objects, starting/killing services, entering records into a database, or even things like defining a URL to query, generating some credentials for a user that doesn't exist yet, or just waiting for some process to finish.
- Act is the singular, state-changing action that kicks off the behavior we want to test. This typically takes the form of a function/method call.
- Assert is where we take that measurement/observation on our test and apply our judgement to it.
- Cleanup is where the test picks up after itself, so other tests aren’t being accidentally influenced by it.
import pytest
class TestStringMethods: # A testcase
# The individual tests are defined with methods whose names start with the
# letters test. This naming convention informs the test runner about which methods
# represent tests.
def test_upper(self):
assert 'foo'.upper() == 'FOO'
def test_isupper(self):
assert 'FOO'.isupper() is True
assert 'Foo'.isupper() is False
def test_split(self):
s = 'hello world'
assert s.split() == ['hello', 'world']
# check that s.split fails when the separator is not a string
with pytest.raises(TypeError):
s.split(2)
Fixture
Fixtures are everything that needs to happen/exist in order to run a test.They're part of the arrange steps.
import pytest
class Fruit:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
@pytest.fixture
def my_fruit():
return Fruit("apple")
@pytest.fixture
def fruit_basket(my_fruit):
return [Fruit("banana"), my_fruit]
def test_my_fruit_in_basket(my_fruit, fruit_basket):
assert my_fruit in fruit_basket
Fixture scopes
Fixtures are created when first requested by a test, and are destroyed based on their scope:
- function: the default scope, the fixture is destroyed at the end of the test.
- class: the fixture is destroyed during teardown of the last test in the class.
- module: the fixture is destroyed during teardown of the last test in the module.
- package: the fixture is destroyed during teardown of the last test in the package.
- session: the fixture is destroyed at the end of the test session.
@pytest.fixture(scope="session")
def smtp_connection():
# the returned fixture value will be shared for
# all tests requesting it
...
Command Line
The pytest module can be used from the command line to run tests from modules, classes or even individual test methods:
pytest test_module1 test_module2
pytest test_module.TestClass
pytest test_module.TestClass.test_method
For a list of all the command-line options:
pytest -h
Assertions
Pytest allows you to use the standard Python assert for verifying expectations and values in Python tests.
Custom error message
Assert supports a message, which should be used to make assert statements more clear.
import pytest
class TestStringMethods:
def test_upper(self):
assert 'foo'.upper() == 'FOO', "Test string uppercase equal"
def test_isupper(self):
assert 'FOO'.isupper() is True, "Test string uppercase True"
assert 'Foo'.isupper() is False, "Test string uppercase False"
def test_split(self):
s = 'hello world'
assert s.split() == ['hello', 'world'], "Test split string"
with pytest.raises(TypeError, match="must be str or None, not int"):
s.split(2)
Parametrize
The builtin pytest.mark.parametrize decorator enables parametrization of arguments for a test function.
import pytest
@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
def test_eval(test_input, expected):
assert eval(test_input) == expected
which can also be declared as
@pytest.mark.parametrize("test_input", ["3+5", "2+4", "6*9"])
@pytest.mark.parametrize("expected", [8, 6, 42])
def test_eval(test_input, expected):
assert eval(test_input) == expected
Anatomy of a Django test
To use Pytest with Django you must install https://pytest-django.readthedocs.io
import pytest
from myapp.models import Animal
@pytest.fixture(scope="function")
def cat():
Animal.objects.create(name="cat", sound="meow")
@pytest.fixture(scope="function")
def lion():
Animal.objects.create(name="lion", sound="roar")
@pytest.fixture(scope="function")
def felines(lion, cat):
...
@pytest.mark.django_db
def test_animals_can_speak(felines):
"""Animals that can speak are correctly identified"""
lion = Animal.objects.get(name="lion")
cat = Animal.objects.get(name="cat")
assert lion.speak() == 'The lion says "roar"'
assert cat.speak() == 'The cat says "meow"'
Why would I use this instead of Django’s manage.py test command?
Running the test suite with pytest offers some features that are not present in Django’s standard test mechanism:
- Less boilerplate: no need to import unittest, create a subclass with methods. Just write tests as regular functions.
- Manage test dependencies with fixtures.
- Run tests in multiple processes for increased speed.
- There are a lot of other nice plugins available for pytest.
- Easy switching: Existing unittest-style tests will still work without any modifications.