igor
igor

Reputation: 260

Unexpected behavior of class scoped fixture when calling a class scoped parametrized fixture in Pytest

I have a test in a class with parametrized indirect class scoped fixture. When another regular class scoped fixture is calling the parametrized fixture, its execution seems to be more function scoped. Meaning the regular fixture is called again, for every test method. When removing the parametrized fixture call, it is executed once per class as expected.

I am aware of fixture life cycle as explained here, but I am not exiting the class. What I am missing here?

pytest-7.0.1, pytest-asyncio-0.17.2

Code:

@pytest_asyncio.fixture(scope='class')
async def some_fixture(request, iteration):  # <---- when not using 'iteration', works as expected
    print(f'-some_fixture-{request.scope}-start')
    yield
    print(f'-some_fixture-{request.scope}-end')

@pytest_asyncio.fixture(scope='class')
async def iteration(self, request):
    print(f'-iteration-{request.scope}-start')
    yield request.param
    print(f'-iteration-{request.scope}-end')

@pytest.mark.parametrize('iteration', range(1, ITERATIONS + 1), indirect=True)  # <---- tried adding scope='class', no success
@pytest.mark.asyncio
class Something:
    async def test_something(self, iteration, some_fixture):
        print(test_body)

Output:

test_foo.py::test_something[1] -iteration-class-start
-some_fixture-class-start
-test_body
PASSED
test_foo.py::test_something[2] -some_fixture-class-end  # <---- why it end here?
-iteration-class-end
-iteration-class-start
-some_fixture-class-start  # <--- called twice
-test_body
PASSED                                                                                                                                                     
-some_fixture-class-end
-iteration-class-end

Upvotes: 1

Views: 423

Answers (1)

MrBean Bremen
MrBean Bremen

Reputation: 16805

This is the expected behavior for parametrized fixtures. A fixture can be directly parametrized (via the params argument in the fixture decorator) or indirectly parametrized by a test (as shown in the question). Both cases are semantically equivalent, and mean that a fixture will be called with different parameters and yield different outcomes. Therefore the fixture has to be evaluated for each parameter separately - you can think of this as a separate fixture for each parameter instead of a single fixture.

The same is true if a fixture (as some_fixture in your example) is "derived" from a parametrized fixture - it will also be parametrized, as a separate outcome is calculated for each parameter. To illustrate this, you can add the parameter to the fixture in the output. Here is a slightly changed example to demonstrate this:

ITERATIONS = 2

@pytest.fixture(scope='class')
def some_fixture(request, iteration):
    print(f'-some_fixture-{request.scope}-{iteration}-start')
    yield
    print(f'-some_fixture-{request.scope}-{iteration}-end')

@pytest.fixture(scope='class')
def iteration(request):
    print(f'-iteration-{request.scope}-{request.param}-start')
    yield request.param
    print(f'-iteration-{request.scope}-{request.param}-end')

@pytest.mark.parametrize('iteration', range(1, ITERATIONS + 1),
                         indirect=True)
class TestSomething:
    def test_something(self, some_fixture):
        print("test_something")

    def test_something_else(self, some_fixture):
        print("test_something_else")

I have removed the async part, as it is not relevant for the question, added the parameter to the fixture output and added a second test in the class (also adapted the test names to be recognized by pytest).

The output for this is:

-iteration-class-1-start
-some_fixture-class-1-start
PASSED test_something
PASSED test_something_else
-some_fixture-class-1-end
-iteration-class-1-end
-iteration-class-2-start
-some_fixture-class-2-start
PASSED test_something
PASSED test_something_else
-some_fixture-class-2-end
-iteration-class-2-end

Note that the fixture is called only once for each parameter, even if it is used multiple times (in this case twice in the 2 tests). As you can see, it indeed behaves like a class fixture, if fixtures with different parameters are seen as different fixtures (as they should).

Upvotes: 1

Related Questions