-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Description
Description
There appears to be an issue with pytest's test execution order that causes a session-scoped, parametrized fixture to be set up and torn down multiple times with the same parameters within a single test session. This behavior is contrary to the expectation that a session-scoped fixture instance should be created only once per unique parameter set and reused for all tests that require it.
Steps to Reproduce
The issue can be observed with the provided test file. The heavy_fixture
is session-scoped and parametrized.
import logging
import pytest
import collections
logger = logging.getLogger(__name__)
HW_IDS: list[str] = [
"FIRST",
"SECOND",
]
SECOND_HW_PARAM: list[str] = ["!ZZZ!", "!XXX!"]
OTHER_PARAMETERS: list[str] = ["p_1_1", "p_1_2"]
OTHER_PARAMETERS_2: list[str] = ["p_2_1", "p_2_2"]
calls_cache = collections.Counter()
@pytest.fixture(scope="session")
def heavy_fixture(request: pytest.FixtureRequest) -> str:
param1, param2 = request.param
calls_cache[(param1, param2)] += 1
logger.info(f" ************ {param1} {param2} - {calls_cache[(param1, param2)]}")
return f"{param1}/{param2}"
@pytest.fixture(scope="function", autouse=True)
def print_current_test_name(request: pytest.FixtureRequest):
yield
logger.info(f"-> {request.node.name}")
@pytest.fixture(scope="session", autouse=True)
def print_calls_cache():
yield
logger.info(" Fixture calls cache:")
for (param1, param2), calls in calls_cache.items():
logger.info(f" - {param1} {param2} -> {calls}")
logger.info(f" Total times fixtures called: {sum(calls_cache.values())}")
logger.info(f" Total heavy fixtures: {len(calls_cache)}")
@pytest.fixture(scope="module")
def fixture_1() -> str:
return "fixture_1"
@pytest.fixture(scope="module")
def fixture_param_1(request: pytest.FixtureRequest) -> str:
return request.param
@pytest.mark.parametrize(
"heavy_fixture",
[(m, b) for m in HW_IDS for b in SECOND_HW_PARAM],
ids=lambda p: f"{p[0]}/{p[1]}",
indirect=["heavy_fixture"],
)
def test_one_fxt(heavy_fixture: str, fixture_1):
pass
@pytest.mark.parametrize("config", OTHER_PARAMETERS)
@pytest.mark.parametrize(
"heavy_fixture",
[(HW_IDS[0], SECOND_HW_PARAM[0])],
ids=lambda p: f"{p[0]}/{p[1]}",
indirect=["heavy_fixture"],
)
def test_with_params_one(heavy_fixture: str, config: str):
pass
@pytest.mark.parametrize("config", OTHER_PARAMETERS)
@pytest.mark.parametrize("config_2", OTHER_PARAMETERS_2)
@pytest.mark.parametrize(
"heavy_fixture",
[(HW_IDS[0], SECOND_HW_PARAM[1])],
ids=lambda p: f"{p[0]}/{p[1]}",
indirect=["heavy_fixture"],
)
def test_with_params_one_second_param(heavy_fixture: str, config: str, config_2: str):
pass
@pytest.mark.parametrize("config", OTHER_PARAMETERS)
@pytest.mark.parametrize(
"heavy_fixture",
[(HW_IDS[0], b) for b in SECOND_HW_PARAM],
ids=lambda p: f"{p[0]}/{p[1]}",
indirect=["heavy_fixture"],
)
def test_with_params_all_models(heavy_fixture: str, config: str):
pass
ITEMS_DATA = [
((hw, b), "fp_1") for hw in HW_IDS for b in SECOND_HW_PARAM
]
ITEMS_DATA_2 = [
((hw, b), "fp_2") for hw in HW_IDS for b in SECOND_HW_PARAM
]
def multiple_fixture_parametrize(
items: tuple[tuple[str, str], str]
):
return pytest.mark.parametrize(
"heavy_fixture, fixture_param_1",
items,
indirect=["heavy_fixture", "fixture_param_1"],
ids=[f"{item[0][0]}/{item[0][1]}/{item[1]}" for item in items]
)
@multiple_fixture_parametrize(ITEMS_DATA)
def test_multiple_fixture_parametrize(heavy_fixture: str, fixture_param_1: str):
pass
@multiple_fixture_parametrize(ITEMS_DATA_2)
def test_multiple_fixture_parametrize_second(heavy_fixture: str, fixture_param_1: str):
pass
Actual Behavior
When running the tests, the output shows that heavy_fixture
is called multiple times for the same parameters. For example, heavy_fixture
with parameters ('FIRST', '!ZZZ!')
is instantiated three separate times during the session.
Execution Log Snippet:
************ FIRST !ZZZ! - 1
> test_one_fxt[FIRST/!ZZZ!]
> test_with_params_one[FIRST/!ZZZ!-p_1_1]
> test_with_params_one[FIRST/!ZZZ!-p_1_2]
************ FIRST !XXX! - 1
> test_with_params_one_second_param[FIRST/!XXX!-p_2_1-p_1_1]
> test_with_params_one_second_param[FIRST/!XXX!-p_2_1-p_1_2]
> test_with_params_one_second_param[FIRST/!XXX!-p_2_2-p_1_1]
> test_with_params_one_second_param[FIRST/!XXX!-p_2_2-p_1_2]
************ FIRST !ZZZ! - 2
> test_with_params_all_models[FIRST/!ZZZ!-p_1_1]
> test_with_params_all_models[FIRST/!ZZZ!-p_1_2]
************ FIRST !XXX! - 2
> test_one_fxt[FIRST/!XXX!]
> test_with_params_all_models[FIRST/!XXX!-p_1_1]
> test_with_params_all_models[FIRST/!XXX!-p_1_2]
************ SECOND !ZZZ! - 1
> test_one_fxt[SECOND/!ZZZ!]
************ SECOND !XXX! - 1
> test_one_fxt[SECOND/!XXX!]
************ FIRST !ZZZ! - 3
> test_multiple_fixture_parametrize[FIRST/!ZZZ!/fp_1]
> test_multiple_fixture_parametrize_second[FIRST/!ZZZ!/fp_2]
************ FIRST !XXX! - 3
> test_multiple_fixture_parametrize[FIRST/!XXX!/fp_1]
> test_multiple_fixture_parametrize_second[FIRST/!XXX!/fp_2]
************ SECOND !ZZZ! - 2
> test_multiple_fixture_parametrize[SECOND/!ZZZ!/fp_1]
> test_multiple_fixture_parametrize_second[SECOND/!ZZZ!/fp_2]
************ SECOND !XXX! - 2
> test_multiple_fixture_parametrize[SECOND/!XXX!/fp_1]
> test_multiple_fixture_parametrize_second[SECOND/!XXX!/fp_2]
Final Fixture Call Count:
Fixture calls cache:
- FIRST !ZZZ! -> 3
- FIRST !XXX! -> 3
- SECOND !ZZZ! -> 2
- SECOND !XXX! -> 2
Total times fixtures called: 10
Total heavy fixtures: 4
Expected Behavior
Pytest should group all tests using the same session-scoped fixture instance together to avoid re-initialization. The heavy_fixture
for a given parameter set (e.g., ('FIRST', '!ZZZ!')
or ('FIRST', '!XXX!')
) should be set up only once for the entire test session.
Expected Execution Log:
************ FIRST !ZZZ! - 1
> test_one_fxt[FIRST/!ZZZ!]
...
> test_with_params_all_models[FIRST/!ZZZ!-p_1_1]
...
> test_multiple_fixture_parametrize[FIRST/!ZZZ!/fp_1]
...
************ FIRST !XXX! - 1
...
Expected Final Fixture Call Count:
Fixture calls cache:
- FIRST !ZZZ! -> 1
- FIRST !XXX! -> 1
- SECOND !ZZZ! -> 1
- SECOND !XXX! -> 1
...
Additional Context
Interestingly, the execution order changes if the test function test_with_params_all_models
is moved before other tests in the file (e.g., to line 97). When moved, the grouping for the ('FIRST', '!XXX!')
parameter set behaves as expected (at least for this test), and its fixture is called one time less. This suggests that the test ordering algorithm is sensitive to the declaration order of tests, which can lead to inefficient fixture management.
How to run
[pytest]
log_cli = true
log_cli_level = DEBUG
pytest -v | grep 'INFO'
Environment
- pytest 8.4.1
- Ubuntu 24.04