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:

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:

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:

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.