PyTest Fixture

Introduction

In this article you can get acquainted with PyTest fixtures.

Official documentation

Shared variable

With a fixture you can share a variable between the tests in a module.

To do that, set the fixture scope to module

import random def generate_word(): route_name = "" symbols = ["A", "E", "H", "K", "L", "M", "N", "T", "4", "7", "8"] for _ in range(5): route_name += random.choice(symbols) return route_name

import pytest import json from utils import generate_word @pytest.fixture(scope="module") def shared_word(): return generate_word() def test_save_word(shared_word): data = {"value": shared_word} with open("output.json", "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) def test_word_is_saved(shared_word): with open("output.json", "r", encoding="utf-8") as f: data = json.load(f) assert data["value"] == shared_word

python -m pytest -vv test_fixt.py

============================= test session starts ============================= collecting ... collected 2 items fixtures/test_fixt.py::test_save_word PASSED [ 50%] fixtures/test_fixt.py::test_word_is_saved PASSED [100%] ============================== 2 passed in 0.02s ============================== Process finished with exit code 0

Changing the shared variable

Quite often you need not only to read the same value in different tests, but also to change it.

A typical case is sequential tests; they are nothing to be afraid of where they are really needed.

The fixture then has to return some mutable object. Usually it is a dictionary.

import pytest @pytest.fixture(scope="module") def shared(): return {} def test_one(shared): shared["value"] = 1 assert isinstance(shared["value"], int) def test_two(shared): assert 2 == shared["value"] + 1 def test_three(shared): print("\n", shared) assert isinstance(shared, dict)

============================= test session starts ============================= collecting ... collected 3 items fixtures/shared.py::test_one PASSED [ 33%] fixtures/shared.py::test_two PASSED [ 66%] fixtures/shared.py::test_three PASSED [100%] {'value': 1} ============================== 3 passed in 0.01s ==============================

You can read about the isinstance() function here .

The same result can be achieved with a list .

Let us replace the object returned by the fixture with an empty list. For a change, let us modify the list in the second test too.

import pytest @pytest.fixture(scope="module") def shared(): return [] def test_one(shared): shared.append(1) assert isinstance(shared[0], int) def test_two(shared): shared[0] = shared[0] + 1 shared.append(8) assert 2, 8 == (shared[0], shared[1]) def test_three(shared): print("\n", shared) assert isinstance(shared, list)

============================= test session starts ============================= collecting ... collected 3 items fixtures/shared.py::test_one PASSED [ 33%] fixtures/shared.py::test_two PASSED [ 66%] fixtures/shared.py::test_three PASSED [100%] [2, 8] ============================== 3 passed in 0.01s ==============================

function

For a fixture to be used by every test, set the scope to function

In the next example we print text to the screen before every test. For that the -s flag is needed.

import pytest @pytest.fixture(scope="function") def announce_new_test(): print("\n\nINFO: TestSetup.ru ---- new test case ----\n") def test_one(announce_new_test): assert 1 == 1 def test_two(announce_new_test): assert 2 == 2

python -m pytest -sv test_fixt.py

====================== test session starts ====================== platform win32 -- Python 3.12.10, pytest-9.0.1, pluggy-1.6.0 -- C:\A\pytest\venv\Scripts\python.exe cachedir: .pytest_cache rootdir: C:\A\pytest plugins: order-1.3.0, rerunfailures-16.1 collected 2 items fixtures/f.py::test_one INFO: TestSetup.ru ---- new test case ---- PASSED fixtures/f.py::test_two INFO: TestSetup.ru ---- new test case ---- PASSED ======================= 2 passed in 0.01s =======================

With this output the percentages [ 50%] and [100%] are lost.

PyCharm can show both the percentages and stdout. I have not learned how yet. If you know how to do it, write to Telegram.

Let us change the code so that the fixture is used automatically and prints the name of each test at the beginning and at the end.

import pytest @pytest.fixture(autouse=True) def announce_new_test(request): print(f"\n\nTEST: {request.node.name}\n{request.node.nodeid}\n") yield print(f"\nEND OF: {request.node.name} {request.node.nodeid}\n\n") def test_one(): assert 1 == 1 def test_two(): assert 2 == 2

============================= test session starts ============================= … collected 2 items fixtures/test_fixt.py::test_one TEST: test_one fixtures/test_fixt.py::test_one PASSED END OF: test_one fixtures/test_fixt.py::test_one fixtures/test_fixt.py::test_two TEST: test_two fixtures/test_fixt.py::test_two PASSED END OF: test_two fixtures/test_fixt.py::test_two ============================== 2 passed in 0.01s ==============================

Test order

To control the order in which the tests run, install the pytest-order library

python -m pip install pytest-order

Collecting pytest-order Downloading pytest_order-1.3.0-py3-none-any.whl.metadata (7.7 kB) Requirement already satisfied: pytest>= 6.2.4 in c:\a\pytest\venv\lib\site-packages (from pytest-order) (9.0.1) Requirement already satisfied: colorama>= 0.4 in c:\a\pytest\venv\lib\site-packages (from pytest>= 6.2.4->pytest-order) (0.4.6) Requirement already satisfied: iniconfig>= 1.0.1 in c:\a\pytest\venv\lib\site-packages (from pytest>= 6.2.4->pytest-order) (2.3.0) Requirement already satisfied: packaging>= 22 in c:\a\pytest\venv\lib\site-packages (from pytest>= 6.2.4->pytest-order) (25.0) Requirement already satisfied: pluggy<2,>= 1.5 in c:\a\pytest\venv\lib\site-packages (from pytest>= 6.2.4->pytest-order) (1.6.0) Requirement already satisfied: pygments>= 2.7.2 in c:\a\pytest\venv\lib\site-packages (from pytest>= 6.2.4->pytest-order) (2.19.2) Downloading pytest_order-1.3.0-py3-none-any.whl (14 kB) Installing collected packages: pytest-order Successfully installed pytest-order-1.3.0

With @pytest.mark.order() you can set the order of a test explicitly.

Let us write two tests and make the second one (the lower one) run before the first one (the upper one in the code).

import pytest @pytest.mark.order(2) def test_one(): assert 1 == 1 @pytest.mark.order(1) def test_two(): assert 2 == 2

python -m pytest -vv test_order.py

================================= test session starts ================================= platform win32 -- Python 3.12.10, pytest-9.0.1, pluggy-1.6.0 -- C:\A\pytest\venv\Scripts\python.exe cachedir: .pytest_cache rootdir: C:\A\pytest plugins: order-1.3.0 collected 2 items fixtures/test_order.py::test_two PASSED [ 50%] fixtures/test_order.py::test_one PASSED [100%] ================================== 2 passed in 0.01s ==================================

Rerunning tests

To rerun failed tests, install the pytest-rerunfailures library

python -m pip install pytest-rerunfailures

Collecting pytest-rerunfailures Downloading pytest_rerunfailures-16.1-py3-none-any.whl.metadata (21 kB) Requirement already satisfied: packaging>=17.1 in c:\autotest\sandbox\python\pytest\venv\lib\site-packages (from pytest-rerunfailures) (25.0) Requirement already satisfied: pytest!=8.2.2,>=7.4 in c:\autotest\sandbox\python\pytest\venv\lib\site-packages (from pytest-rerunfailures) (9.0.1) Requirement already satisfied: colorama>=0.4 in c:\autotest\sandbox\python\pytest\venv\lib\site-packages (from pytest!=8.2.2,>=7.4->pytest-rerunfailures) (0.4.6) Requirement already satisfied: iniconfig>=1.0.1 in c:\autotest\sandbox\python\pytest\venv\lib\site-packages (from pytest!=8.2.2,>=7.4->pytest-rerunfailures) (2.3.0) Requirement already satisfied: pluggy<2,>=1.5 in c:\autotest\sandbox\python\pytest\venv\lib\site-packages (from pytest!=8.2.2,>=7.4->pytest-rerunfailures) (1.6.0) Requirement already satisfied: pygments>=2.7.2 in c:\autotest\sandbox\python\pytest\venv\lib\site-packages (from pytest!=8.2.2,>=7.4->pytest-rerunfailures) (2.19.2) Downloading pytest_rerunfailures-16.1-py3-none-any.whl (14 kB) Installing collected packages: pytest-rerunfailures

With @pytest.mark.flaky you can set how many times and at what interval a test is rerun.

Suppose we are testing an application that loads slowly.

Say the timeout is 12 seconds and we need to make sure the application has loaded within that time.

We do not want to wait 12 seconds every time. The application may load in 6 or even in 3 seconds.

Let us check after 3 seconds. On the first attempt it may have loaded or it may not.

If it has not loaded, that does not mean a bug has been found. We wait another 3 seconds, then another, and so on up to 12.

To simulate such an application I took the function from the article «Call counter»

# apps.py def slow_app(): slow_app.calls += 1 loaded = 0 if slow_app.calls > 3: loaded = 1 return loaded slow_app.calls = 0

The test will look like this

import pytest from apps import slow_app @pytest.mark.flaky(reruns=4, reruns_delay=3) def test_app_is_loaded(): loaded = slow_app() assert loaded == 1

=========================== test session starts ============================ platform win32 -- Python 3.12.10, pytest-9.0.1, pluggy-1.6.0 -- C:\A\pytest\venv\Scripts\python.exe cachedir: .pytest_cache rootdir: C:\A\pytest plugins: order-1.3.0, rerunfailures-16.1 collected 1 item fixtures/test_flaky.py::test_app_is_loaded RERUN [100%] fixtures/test_flaky.py::test_app_is_loaded RERUN [100%] fixtures/test_flaky.py::test_app_is_loaded RERUN [100%] fixtures/test_flaky.py::test_app_is_loaded PASSED [100%] ======================== 1 passed, 3 rerun in 9.05s ========================

The logs show that our stub application «loaded» in 9 seconds, which fits into 12, so the test passed although with three reruns.

Suite setup and teardown

One of the typical uses of fixtures, not only in PyTest but also in other frameworks, is preparing and tearing down the environment.

The counterpart of what Robot Framework does with Suite Setup and Suite Teardown is one fixture split into two parts with yield.

An example of such a fixture, which imports the functions start_app() and close_app() and uses them at the beginning and at the end of the test session.

from tests.suite_setup import start_app from tests.suite_teardown import close_app @pytest.fixture(scope="session", autouse=True) def suite_setup_teardown(): start_app() yield close_app()

Article author: Arthur Isaev