Search code examples
pythonpytestfixtures

Caching of parameterized, nested fixtures in pytest


I am trying to understand how and when return values from pytest fixtures are cached. In my understanding, the goal of fixtures (in particular session-scoped fixtures) is that they are called only once and that return values are cached for future calls. This does not seem to be the case for nested parameterized fixtures.

The following code shows the issue:

from collections import Counter

import pytest

state = Counter()


@pytest.fixture(scope="session", autouse=True)
def setup_session():
    yield None
    print(state)


@pytest.fixture(scope="session", params=["A1", "A2", "A3"])
def first(request):
    state[request.param] += 1
    return request.param


@pytest.fixture(scope="session", params=["B1", "B2"])
def second(first, request):
    return first + request.param


@pytest.fixture(scope="session", params=["C1", "C2"])
def third(second, request):
    return second + request.param


def test_length(third):
    assert len(third) == 6

The output is Counter({'A1': 3, 'A2': 2, 'A3': 1}). So first is called three times for parameter value A1, two times for A2, and once for A3. Why?

I am expecting to get Counter({'A1': 1, 'A2': 1, 'A3': 1}) - one call for each parameter value.

In case that's relevant, I am using Python 3.12.3, pytest-8.3.4

PS: All 12 tests pass - that's fine. PPS: When removing one level of nesting, the problem disappears and first is called exactly once per parameter value.


Solution

  • From the docs (Note box)

    Pytest only caches one instance of a fixture at a time, which means that when using a parametrized fixture, pytest may invoke a fixture more than once in the given scope.

    If you look closely at the console output you will see that you count the changes in the returned value from first

    test_length[C1-B1-A1] -> {'A1': 1, 'A2': 0, 'A3': 0}
    test_length[C1-B1-A2] -> {'A1': 1, 'A2': 1, 'A3': 0}
    test_length[C1-B2-A2]
    test_length[C2-B2-A2]
    test_length[C2-B2-A1] -> {'A1': 2, 'A2': 1, 'A3': 0}
    test_length[C2-B2-A3] -> {'A1': 2, 'A2': 1, 'A3': 1}
    test_length[C2-B1-A3]
    test_length[C1-B2-A3]
    test_length[C1-B1-A3]
    test_length[C2-B1-A2] -> {'A1': 2, 'A2': 2, 'A3': 1}
    test_length[C2-B1-A1] -> {'A1': 3, 'A2': 2, 'A3': 1}
    test_length[C1-B2-A1]
    

    PPS: reducing the nesting might give you the expected results, but not necessarily

    def test_length(second):
    

    output was Counter({'A1': 2, 'A2': 1, 'A3': 1})