PyTest
PyTest is a testing framework based on Python.
Some features/advantages:
Can run multiple tests in parallel
Allows you to skip/run a subset of tests from the testsuite
Free and opensource!
Running PyTest without specifying filenames will run all files of format test_*.py or *_test.py
in the current directory or subdirectories.
PyTest test function names must start with test or test_.
Basic Test Example
import math
def test_sqrt():
num = 25
assert math.sqrt(num) == 5
def testsquare():
num = 7
assert 7*7 == 40
# Note: this won't run since it doesn't start with "test"
def tesequality():
assert 10 == 11
Run pytest in the same directory level.
Test Subsets
We don’t always want to specify all the tests to run/don’t want all tests to run automatically.
Can run a subset of tests in two ways:
String matching of test name:
pytest -k great -vwill only run tests with ‘great’ in their nameTest Markers:
@pytest.mark.<markername>pytest -m <markername> -v
Test Classes
You can specify a class to group tests. This also allows you share parameters between
tests, like value in the example.
# content of test_class_demo.py
class TestClassDemoInstance:
value = 0
def test_one(self):
self.value = 1
assert self.value == 1
def test_two(self):
assert self.value == 1
Grouping in classes also allows you to apply class level fixtures and marks and these will be implicitly added to all tests in that class.
Fixtures
Fixtures are functions which run before each test function to which it is applied. They can be used to feed data to a test function.
The scope is limited to the file you have created it in by default. However, if it is included in your conftest.py file, you can change the scope.
You can set the scope of a fixture to session, module, function, class, package by using @pytest.fixture(scope='session')
You can also set autouse=True if you want it enabled for all test functions in the scope.
import pytest
@pytest.fixture
def input_value():
input = 39
return input
def test_divisible_by_3(input_value):
assert input_value % 3 == 0
def test_divisible_by_6(input_value):
assert input_value % 6 == 0
Instead of passing the fixture as an argument, you can also use the @pytest.mark.usefixtures('<fixture_name>')
decorator to use the fixture in a test function.
Note
You can view a list of builtin fixtures by using pytest --fixtures. These are ones
you don’t have to specify but can just use the name of the fixture e.g. tmp_path
Using fixtures for setup and teardown
You can use fixtures for setup and teardown functions for tests.
Using it as a setup is useful since you can pass in data to your test function.
Using the teardown fixture is useful, since on an assert inside your test function, the remaining
functionality won’t be run, meaning if you have teardown logic inside the test, it won’t be run on
an assert.
@pytest.fixture(scope="function")
def test_setup():
print("Test Specific Setup")
test_data = 5
return test_data
@pytest.fixture(scope="function")
def test_teardown():
yield
print("Test Specific Teardown")
# test_teardown doesn't provide a value so can be declared like this
@pytest.usefixtures('test_teardown')
def test_example(test_setup):
assert test_setup == 3
It is also possible to chain fixtures:
@pytest.fixture(scope="function")
def test_setup():
print("Test Specific Setup")
test_data = 5
return test_data
@pytest.fixture(scope="function")
def test_teardown(test_setup):
yield test_setup
print("Test Specific Teardown")
def test_example(test_teardown):
assert test_teardown == 3
In this example, the setup will run first, then the teardown yields to the test.
Once the test is complete, due to the yield statement, the rest of the teardown
is run.
Choosing fixtures at run-time
You can select which fixtures to run during runtime with pytest.
return request.getfixturevalue(argname=fixture_name)
conftest.py
Can use the file conftest.py to share configurations across test files, e.g. fixtures.
import pytest
@pytest.fixture
def input_value():
input = 39
return input
Now this input_value can be used across all test files.
Running Tests in Parallel
To run tests in parallel, you need the pytest-xdist plugin.
pip install pytest-xdist
Now you can specify how many workers you want to use to run the tests in parallel:
pytest -n <number_of_workers>
Markers
Set a test group that can run as a subset:
@pytest.mark.<markername>You can register your custom marks in the
pytest.inifile. Unregistered marks will trigger a warning. This warning can be raised to an error by passing thestrict-markersargument. This avoids accidental mark name errors. Or add something like this to your pytest.ini[pytest] addopts = --strict-markers
Note
You can also use the other marker names specified here to run a subset too e.g. xfail
Set a set of inputs to test for a function:
import pytest
@pytest.mark.paramterize("num, ouput", [(1,11),(2,22),(3,35)])
def test_multiplication_11(num, output):
assert 11*num == output
Execute a test, but don’t consider its result:
@pytest.mark.xfailDon’t execute a test:
@pytest.mark.skip. Add(reason="<reson>")to give a skip reason.
Note
You can view a list of markers by using pytest --markers
Test Execution Results
The results from the test execution can be written to an XML file. This can be used in a dashboard that displays test results.
pytest test_multiplication.py -v --junitxml="results.xml"
PyTest Hooks
Hooks are part of PyTest’s plugin system which allows you to extend the functionality of PyTest Hooks allows you to run custom code at different stages of a pytest run.
The stage at which the code is run is defined by the name of the hook you use. These are predefined names which point to a different stage in the pytest process.
To define a hook, create a function and decorate it with @pytest.hookimpl.
Note
It doesn’t seem hooks can be declared in the test file, but work in the conftest.py file.
There are some main categories of hooks:
Bootstrapping hooks
Called at the very beginning and end of a test run.
Good for setup and teardown of resources used by all tests
Initialisation hooks
Called at beginning of a test run, after bootstrapping hooks.
Used to initialise resources before the test run
Collection hooks
Called during the process of collecting the tests that will be run in the test suite
Can sutomise the way that tests are collected and add additional tests to the collection
Test running (runtest) hooks
Customise the way tests are run and perform actions before and after a test is run
Example in conftest.pyimport pytest @pytest.hookimpl def pytest_runtest_setup(item): print("Setting up test:", item.name) # Perform setup tasks here
Reporting hooks
Called throughout the test process
Customise the way results are reported
Allows you perform actions based on test results
Debugging/Interaction Hooks
Allows us to interact with the test session or debug issues that might arise
You can find a list of available hooks Here. These are the names you should use to target a specific part of the PyTest process.
Note
Hooks should start with pytest_* otherwise it won’t be recognised as a hook.
@pytest.hookimpl
def pytest_collection_modifyitems(config, items):
# modify the collected items after they have been collected...
items.append(items[0])
You can also make a function a hookwrapper so that it will wrap another hook function. In the example,
the hookwrapper function is called first. The yield statement yields to the wrapped hook function. When
that finishes, execution returns to the hookwrapper to complete:
@pytest.hookimpl(hookwrapper=True)
def pytest_collection_modifyitems(config, items):
print('Entering the collection_modifyitems hook')
yield
print('Finished the collection_modifyitems hook')
Options
-v: make the output more verbose-k <substring>: run a subset of tests based on given substring-m <markername>: only run tests with given marker--maxfail <max_number_of_fails>: Number of fails after which to halt test execution-n <num_of_workers>: How many parallel workers will run the tests--junittxml=<path_to_file>: Outputs test results to XML-s: This will showprint()from test functions in the console
Running multiple instances of pytest on the same code
There might be a situation where you want to run multiple configs for pytest on the same repo.
To avoid using the same config, use a pytest.ini file to root the pytest instance you are calling.
This can cause an issue with your code trying to find some libraries in higher directory levels.
One thing that might help, is to run python -m pytest ... instead of pytest directly. They
are mostly the same except the first one adds more paths than just pytest by itself.
Output Parsing
Use the junitparser package to parse output results files produced by pytest with the
--junit-xml=path option.
e.g. from junitparser import JUnitXml
Command line arguments
You can pass in arguments to pytest in the pytest call.
# included in conftest.py
def pytest_addoption(parser):
parser.addoption(
"--access_token",
action="store",
default="",
help="token for downloading docker image",
)
parser.addoption(
"--use_local_image",
action="store_true",
default=False,
help="Flag for using local docker image",
)
The above example shows how you can make a simple flag or a argument with a value.
You can view the argument by using the request fixture: request.config.getoption("--access_token")
Note
It is also possible to add options from the pytest.ini file:
addopts = --ignore=./dummy_tests in the [pytest] section.