Fixture Parametrization
Purpose
Fixture Parametrization adds powerful support for running a single test multiple times with different input values provided by fixtures. This subtopic addresses the need to:
Automatically generate multiple test invocations from one fixture with various parameters.
Enable tests to receive different data sets or configurations without duplicating test code.
Integrate seamlessly with the fixture lifecycle and scope mechanisms to manage setup and teardown efficiently per parameter set.
While the parent topic covers the flexible lifecycle and dependency management of fixtures, Fixture Parametrization specifically enables fixtures to define multiple parameter values, causing tests depending on them to run once per parameter. This extends pytest’s ability to express combinatorial and data-driven test scenarios cleanly.
Functionality
Key workflows and mechanisms unique to Fixture Parametrization include:
1. Defining Parametrized Fixtures
Fixtures can declare a `params` argument in the `@pytest.fixture` decorator, which is a sequence of values to parameterize over:
@pytest.fixture(params=[1, 2, 3])
def sample_data(request):
return request.param
Each value in `params` results in a separate fixture invocation and consequently a separate test call.
2. Accessing Current Parameter via request.param
Inside a parametrized fixture, the special `request` object exposes the current parameter via `request.param`. This allows test code or fixtures to adapt behavior based on the parameter:
def test_example(sample_data):
assert sample_data in [1, 2, 3]
3. Dynamic Test Generation for Parametrized Fixtures
During test collection, pytest’s `FixtureManager` inspects fixtures used by a test function (`metafunc`) and detects if any requested fixtures are parametrized. It then calls `metafunc.parametrize()` with the fixture’s parameters to generate multiple test calls, each with a distinct fixture parameter:
def pytest_generate_tests(self, metafunc: Metafunc) -> None:
for argname in metafunc.fixturenames:
fixture_defs = metafunc._arg2fixturedefs.get(argname)
if not fixture_defs:
continue
for fixturedef in reversed(fixture_defs):
if fixturedef.params is not None:
metafunc.parametrize(
argname,
fixturedef.params,
indirect=True,
scope=fixturedef.scope,
ids=fixturedef.ids,
)
break
4. Parametrization Scope and Caching
Each parameter value is associated with a cache key used to identify the fixture instance. Fixture caching and teardown are managed per parameter:
The
FixtureDef.execute()method compares the current parameter to the cached one.If the parameter changed, the previous fixture instance is torn down and a fresh fixture setup is performed.
The fixture’s scope (e.g., function, module) influences how parameters are grouped and cached.
5. Reordering Test Items to Minimize Setup Overhead
Parametrized tests are reordered by `reorder_items()` to group tests sharing common higher-scope parameter values together, minimizing expensive setup/teardown cycles at broader scopes.
This involves generating `ParamArgKey` objects representing parameter identity and sorting test items accordingly with a recursive algorithm.
Relationship
Fixture Parametrization is tightly integrated with the overall Fixture Management system, complementing:
Fixture Lifecycle: Parametrization extends lifecycle management by adding caching and teardown per parameter value.
Test Discovery and Collection: Parametrized fixtures influence test item generation and parameter combinations.
Test Execution: Each generated test call corresponds to a unique fixture parameter combination executed via the test runner.
Plugin System and Hooks: Hooks such as
pytest_generate_testsare used to implement parametrized test generation.
It introduces new behaviors not covered by the general fixture lifecycle, namely the generation of multiple test calls from a single fixture definition with varying parameters and the management of fixture values per parameter instance.
Diagram
The following flowchart illustrates the core process of fixture parametrization from fixture definition to test generation and execution:
flowchart TD
A[Define Fixture with params] --> B[FixtureManager detects params]
B --> C[pytest_generate_tests called with Metafunc]
C --> D[Metafunc.parametrize invoked with fixture params]
D --> E[Test Collection generates multiple test calls]
E --> F[Each test call receives distinct fixture param]
F --> G[Fixture executed per param, caching per param]
G --> H[Test runs with fixture value]
H --> I[Fixture teardown after param group exhausted]
This flow shows how parametrized fixtures result in multiple test instances, each driven by a different parameter value, with caching and teardown handled accordingly.
Code Snippet Highlight
Key code snippet from `FixtureManager.py` illustrating parametrized fixture detection and test generation:
def pytest_generate_tests(self, metafunc: Metafunc) -> None:
for argname in metafunc.fixturenames:
fixture_defs = metafunc._arg2fixturedefs.get(argname)
if not fixture_defs:
continue
for fixturedef in reversed(fixture_defs):
if fixturedef.params is not None:
metafunc.parametrize(
argname,
fixturedef.params,
indirect=True,
scope=fixturedef.scope,
ids=fixturedef.ids,
)
break
This method dynamically creates multiple test calls corresponding to each fixture parameter, integrating parametrization deeply into pytest’s test generation.
Fixture Parametrization thus provides a seamless, powerful approach to writing concise, reusable tests that operate over varying data, enhancing pytest's flexibility beyond simple fixture lifecycle management.