Mathew K Analytics

Lesson 10 · Pytest deep dive

pytest Tutorial #10: Parametrised & Indirect Fixtures

On testing real Python code with pytest. Fixtures with params, request.param, and indirect parametrization. Let's get into it. @pytest.fixture(params=[...])…

⬇ Download notebook

📓 Full notebook

Download .ipynb

pytest Deep-Dive, Video 10: Parametrized and Indirect Fixtures#

  • Video ten of an eighteen-part series on testing real Python code with pytest.
  • Fixtures with params, request.param, and indirect parametrization.
  • Let's get into it.

Part 1: The Helper From Last Time#

import pytest, tempfile, os
def run_pytest(code_str, args=None):
    with tempfile.NamedTemporaryFile(mode='w', suffix='_test.py', delete=False) as f:
        f.write(code_str)
        path = f.name
    result = pytest.main([path, *(args or [])])
    os.unlink(path)
    return result

Part 2: A Fixture With params - request.param#

code = '''
import pytest

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

def test_number_is_positive(number):
    assert number > 0

def test_number_squared(number):
    assert number * number >= number
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 6 items

..\..\..\Users\makmw\AppData\Local\Temp\tmpt0rmx81j_test.py::test_number_is_positive[1] PASSED [ 16%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpt0rmx81j_test.py::test_number_is_positive[2] PASSED [ 33%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpt0rmx81j_test.py::test_number_is_positive[3] PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpt0rmx81j_test.py::test_number_squared[1] PASSED [ 66%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpt0rmx81j_test.py::test_number_squared[2] PASSED [ 83%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpt0rmx81j_test.py::test_number_squared[3] PASSED [100%]

============================== 6 passed in 0.23s ==============================
0

Part 3: Every Test Sharing the Fixture Multiplies Together#

code = '''
import pytest

@pytest.fixture(params=['sqlite', 'postgres', 'mysql'])
def db_backend(request):
    return request.param

def test_backend_is_known(db_backend):
    assert db_backend in ('sqlite', 'postgres', 'mysql')

def test_backend_name_is_lowercase(db_backend):
    assert db_backend == db_backend.lower()

def test_backend_name_not_empty(db_backend):
    assert len(db_backend) > 0
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 9 items

..\..\..\Users\makmw\AppData\Local\Temp\tmp7ask1_or_test.py::test_backend_is_known[sqlite] PASSED [ 11%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp7ask1_or_test.py::test_backend_is_known[postgres] PASSED [ 22%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp7ask1_or_test.py::test_backend_is_known[mysql] PASSED [ 33%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp7ask1_or_test.py::test_backend_name_is_lowercase[sqlite] PASSED [ 44%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp7ask1_or_test.py::test_backend_name_is_lowercase[postgres] PASSED [ 55%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp7ask1_or_test.py::test_backend_name_is_lowercase[mysql] PASSED [ 66%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp7ask1_or_test.py::test_backend_name_not_empty[sqlite] PASSED [ 77%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp7ask1_or_test.py::test_backend_name_not_empty[postgres] PASSED [ 88%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp7ask1_or_test.py::test_backend_name_not_empty[mysql] PASSED [100%]

============================== 9 passed in 0.16s ==============================
0

Part 4: Custom IDs on a Parametrized Fixture#

code = '''
import pytest

@pytest.fixture(params=[0, -1, 100], ids=['zero', 'negative', 'large'])
def edge_case_value(request):
    return request.param

def test_edge_case_is_int(edge_case_value):
    assert isinstance(edge_case_value, int)
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 3 items

..\..\..\Users\makmw\AppData\Local\Temp\tmpnkal3bn0_test.py::test_edge_case_is_int[zero] PASSED [ 33%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpnkal3bn0_test.py::test_edge_case_is_int[negative] PASSED [ 66%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpnkal3bn0_test.py::test_edge_case_is_int[large] PASSED [100%]

============================== 3 passed in 0.12s ==============================
0

Part 5: Indirect Parametrization - Values Flow Through a Fixture#

code = '''
import pytest

@pytest.fixture
def doubled(request):
    return request.param * 2

@pytest.mark.parametrize('doubled', [1, 2, 3], indirect=True)
def test_doubled_value(doubled):
    assert doubled in (2, 4, 6)
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 3 items

..\..\..\Users\makmw\AppData\Local\Temp\tmp51mtmgra_test.py::test_doubled_value[1] PASSED [ 33%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp51mtmgra_test.py::test_doubled_value[2] PASSED [ 66%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp51mtmgra_test.py::test_doubled_value[3] PASSED [100%]

============================== 3 passed in 0.12s ==============================
0

Part 6: Indirect on Just One of Several Parameters#

code = '''
import pytest

@pytest.fixture
def user_record(request):
    name = request.param
    return {'name': name, 'greeting': f'hello, {name}'}

@pytest.mark.parametrize('user_record, expected_length', [
    ('alice', 5),
    ('bob', 3),
], indirect=['user_record'])
def test_user_record_name_length(user_record, expected_length):
    assert len(user_record['name']) == expected_length
    assert user_record['greeting'].startswith('hello,')
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 2 items

..\..\..\Users\makmw\AppData\Local\Temp\tmpud2di3za_test.py::test_user_record_name_length[alice-5] PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpud2di3za_test.py::test_user_record_name_length[bob-3] PASSED [100%]

============================== 2 passed in 0.13s ==============================
0

Part 7: A Real Pattern - Reusable Environment Configs#

code = '''
import pytest

ENVIRONMENTS = [
    {'name': 'dev', 'debug': True, 'workers': 1},
    {'name': 'staging', 'debug': True, 'workers': 2},
    {'name': 'prod', 'debug': False, 'workers': 4},
]

@pytest.fixture(params=ENVIRONMENTS, ids=lambda e: e['name'])
def env_config(request):
    return request.param

def test_env_has_positive_workers(env_config):
    assert env_config['workers'] > 0

def test_prod_has_debug_off(env_config):
    if env_config['name'] == 'prod':
        assert env_config['debug'] is False
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 6 items

..\..\..\Users\makmw\AppData\Local\Temp\tmpue9v4x72_test.py::test_env_has_positive_workers[dev] PASSED [ 16%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpue9v4x72_test.py::test_env_has_positive_workers[staging] PASSED [ 33%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpue9v4x72_test.py::test_env_has_positive_workers[prod] PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpue9v4x72_test.py::test_prod_has_debug_off[dev] PASSED [ 66%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpue9v4x72_test.py::test_prod_has_debug_off[staging] PASSED [ 83%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpue9v4x72_test.py::test_prod_has_debug_off[prod] PASSED [100%]

============================== 6 passed in 0.12s ==============================
0

Wrap-Up: What You Learned#

  • @pytest.fixture(params=[...]) makes the fixture itself run once per value, via request.param.
  • Every test requesting a parametrized fixture automatically multiplies by that fixture value count.
  • ids works on parametrized fixtures too, giving each value a readable label in verbose output.
  • indirect=True on parametrize routes values through a matching fixture instead of straight to the test.
  • indirect also accepts a list of names, routing only those specific parametrize arguments through fixtures.
  • A parametrized fixture of environment configs avoids repeating the same parametrize list on every test.
  • That wraps up parametrized and indirect fixtures. Next up: Plugins.

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.