Fixtures in pytest: All You Need to Know

What are fixtures? They are nothing but regular functions that are run by pytest before executing an actual test function.

Dinesh Kumar K B
Python in Plain English
6 min readJan 13, 2022

--

https://dock2learn.com/tech/fixtures-in-pytest-all-you-need-to-know/

For non-members, this story is available here.

Introduction:

pytest is a software testing framework that can be used in all levels of testing such as unit, functional. It provides command-line options which automatically find and execute your tests. The striking feature of pytest is the plethora of plugins that are available to extend its capability. One such plugin is the fixture.

What are Fixtures?

Fixtures are nothing but regular functions that are run by pytest before executing an actual test function. Basically, you may write the setup code inside fixtures that are required to run your test i.e., If you have any database connections required for your test case to be set up or prep the test data that will be used in the test function.

For any automation tests to run error-free, the initial and end state of the system should be set appropriately. Fixtures help us do that elegantly and efficiently.

Getting started with fixtures:

A simple fixture:

import pytest

@pytest.fixture()
def setup_test_data():
test_value = 100
return test_value


def test_function(setup_test_data):
a = 100
assert a == setup_test_data

Any python function decorated with @pytest.fixture() becomes a pytest fixture.The fixture name is passed as a parameter to the function under test. In the above example, the fixture setup_test_data is passed as a parameter to test_function.

pytest looks for fixtures in the current module. If it doesn’t find there, it also looks for fixtures in conftest.py file.

Sharing fixtures between multiple tests:

If a fixture has to be used across multiple tests, adding them in one single test file and importing may not make sense. Also rewriting them in every single test file would violate the DRY principle.

pytest provides a well-structured test framework that provides a centralized location to write all the fixtures so that you could have a clear demarcation between the function under test and setup and teardown step code which are fixtures in our case.

Now, let’s try rewriting the fixture in conftest.py file and use it across multiple files.

conftest.py

@pytest.fixture
def get_db_driver():
return "psycopg"

test_file1.py

def test_db_1(get_db_driver):
print(get_db_driver)

test_file2.py

def test_db_2(get_db_driver):
print(get_db_driver)

Please note that you don’t have to import the conftest.py module explicitly. pytest fetches the file for us as long as it is available in your project/tests directory. In the above example, both the tests from different test files automatically execute the fixture get_db_driver. These examples are just minor illustrations and the actual use cases would be more convincing to use the fixtures.

The actual execution of fixtures can be passing a switch to the pytest command-line argument.

$pytest --setup-show -s myfixture.py::test_db_1
============== test session starts ============
collected 1 item

myfixture.py
SETUP S _session_faker
SETUP F get_db_driver
myfixture.py::test_db_1 (fixtures used: _session_faker, get_db_driver, request)psycopg
.
TEARDOWN F get_db_driver
TEARDOWN S _session_faker

If you look at the logs, the SETUP and TEARDOWN explain the fixtures used. The -s flag is to say pytest to print to console.

Using Multiple Fixtures:

There may be situations where you may need multiple steps to be set up before executing a test. pytest lets us pass multiple fixtures to a test as arguments.

import pytest


@pytest.fixture
def allowed_input_list():
return [1,2,3,4,5]

@pytest.fixture
def allowed_output_list():
return [10,20,30]


def test_my_fixture(allowed_input_list, allowed_output_list):

input_number = 4
assert input_number in allowed_input_list
assert input_number * 5 in allowed_output_list

This is a plain example of using multiple fixtures in a test.

Scoping fixtures:

Fixtures have optional arguments called scope. The scope parameters have the following values.

As the name implies, the fixture scope defines how frequently they are run. For instance, a function scope fixture is run for every single test. i.e The fixture setup runs before every single test and the teardown is run after every single test. It is evident from the sample output shown in the --setup-show example.

import pytest


@pytest.fixture(scope='class')
def class_scope_fixture():
"Run once per test class"
pass

@pytest.fixture(scope='session')
def session_scope_fixture():
"Run once per test session"
pass

@pytest.fixture(scope='module')
def module_scope_fixture():
"Run once per test module"
pass

Output:

dineshkumarkb@dineshkumarkb:~/$ pytest --setup-show -s
=============== test session starts ==================
platform linux -- Python 3.8.10, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /home/dineshkumarkb/MyGitHub/MyPractice/Python
plugins: Flask-Dance-3.3.1
collected 1 item

test_file.py
SETUP S session_scope_fixture
SETUP M module_scope_fixture
SETUP F function_scope_fixture
Fixture/test_file.py::test_fixture_scope (fixtures used: function_scope_fixture, module_scope_fixture, session_scope_fixture).
TEARDOWN F function_scope_fixture
TEARDOWN M module_scope_fixture
TEARDOWN S session_scope_fixture

Here you could see the execution flow of fixtures with their scope. The session scope fixture is executed first, then the module level, and finally the function level fixture.

Try switching the order of fixtures and still they get executed as per their scope i.e., session-scoped fixture gets executed first, then module fixture, and finally function level fixture.

UseFixtures and AutoUse:

Another way of specifying fixtures for a test is using usefixture. It is more appropriate to use this method for class scope fixtures. In the case of functions, this would add more typing work.

Below is an example of class scope fixtures:

@pytest.mark.usefixtures(class_scope_fixture)
class TestFixture:

def test_1(self):
pass

def test_2(self):
pass

Output:

dineshkumarkb@dineshkumarkb:~/$ pytest --setup-show -s
=================== test session starts ================
platform linux -- Python 3.8.10, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /home/dineshkumarkb/MyGitHub/MyPractice/Python
plugins: Flask-Dance-3.3.1
collected 2 items

test_file.py
SETUP C class_scope_fixture
Fixture/test_file.py::TestFixture::test_1 (fixtures used: class_scope_fixture).
Fixture/test_file.py::TestFixture::test_2 (fixtures used: class_scope_fixture).
TEARDOWN C class_scope_fixture

So far, we specified fixtures manually. We do have fixtures that could run always. Let’s say you want to calculate the execution time of every test. That code could be written inside a fixture and used.

import pytest
from datetime import datetime

@pytest.fixture(autouse=True)
def time_calculator():
print(f" Test started: {datetime.now().strftime('%d-%m-%Y %H:%M')} ")
yield
print(f" Test ended: {datetime.now().strftime('%d-%m-%Y %H:%M')} ")


def test_fixture_scope():
time.sleep(2)

We have added a yield statement in the fixture. Adding a yield statement suspends the execution of a fixture and transfers the execution control to the test. Any code after the yield statement will be executed after the test.

Output:

================= test session starts ===============
platform linux -- Python 3.8.10, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /home/dineshkumarkb/MyGitHub/MyPractice/Python
plugins: Flask-Dance-3.3.1
collected 1 item

test_file.py Test started: 13-01-2022 20:06

SETUP F time_calculator
Fixture/test_file.py::test_fixture_scope (fixtures used: time_calculator). Test ended: 13-01-2022 20:06

TEARDOWN F time_calculator

We have not added the fixture to the test function. However, the fixture got executed. This is the functionality of autouse. Any fixture that has to be executed for all tests can be set as autouse=True.

Parametrizing Fixtures:

Fixtures can be parametrized to send multiple data to a test. Let’s say you want to test one or more functions with multiple sets of data, then you can parametrize the fixtures.

@pytest.fixture(params=[1,2,3,4,5])
def list_of_num(request):
return request.param


def test_fixture_params(list_of_num):
print(list_of_num)

Here request is an inbuilt fixture to handle the parametrization and the calling state of test data.

Output:

dineshkumarkb@dineshkumarkb:~/MyGitHub/MyPractice/Python/Fixture$ pytest --setup-show -s
========================= test session starts ====================
platform linux -- Python 3.8.10, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: /home/dineshkumarkb/MyGitHub/MyPractice/Python
plugins: Flask-Dance-3.3.1
collected 5 items

test_file.py
SETUP F list_of_num[1]
Fixture/test_file.py::test_fixture_params[1] (fixtures used: list_of_num, request)1
.
TEARDOWN F list_of_num[1]
SETUP F list_of_num[2]
Fixture/test_file.py::test_fixture_params[2] (fixtures used: list_of_num, request)2
.
TEARDOWN F list_of_num[2]
SETUP F list_of_num[3]
Fixture/test_file.py::test_fixture_params[3] (fixtures used: list_of_num, request)3
.
TEARDOWN F list_of_num[3]
SETUP F list_of_num[4]
Fixture/test_file.py::test_fixture_params[4] (fixtures used: list_of_num, request)4
.
TEARDOWN F list_of_num[4]
SETUP F list_of_num[5]
Fixture/test_file.py::test_fixture_params[5] (fixtures used: list_of_num, request)5
.
TEARDOWN F list_of_num[5]

Summary:

Fixtures are test functions that can be used to write test setup and tear down steps. pytest also provides built-in fixtures that are helpful. We have discussed sharing fixtures via conftest, adding multiple fixtures to tests and scoping fixtures. Leverage the fixture functionality of pytest to prep your test setup.

Originally published at https://dock2learn.com on January 13, 2022.

More content at plainenglish.io. Sign up for our free weekly newsletter. Get exclusive access to writing opportunities and advice in our community Discord.

--

--