-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Description
We have two collection nodes (and fixture scopes), Package
and Class
, which may be nested. I'll use classes for example because it's easier to discuss (single file) but it's the same for packages.
Consider the following:
import pytest
@pytest.fixture(scope="class")
def fix(request):
print("SETUP", request.node)
yield
print("TEARDOWN", request.node)
class TestTop:
def test_top(self, fix):
print("top")
class TestNested1:
def test_nested1(self, fix):
print("nested1")
class TestNested2:
def test_nested2(self, fix):
print("nested2")
The collection tree is:
<Module x.py>
<Class TestTop>
<Function test_top>
<Class TestNested1>
<Function test_nested1>
<Class TestNested2>
<Function test_nested2>
What should be printed?
(I'm adding some indentation to the outputs to make them clearer).
Option 1 - Current behavior
The current behavior is the following.
SETUP <Class TestTop>
top
nested1
TEARDOWN <Class TestTop>
SETUP <Class TestNested2>
nested2
TEARDOWN <Class TestNested2>
Technical details why it happens
Each fixture definition is pytest becomes a FixtureDef
instance. In the example we have one FixtureDef
, for fix
.
In the current implementation, the FixtureDef
itself holds a fixture's value (cached_value
). When a fixture value is requested (SubRequest.getfixturevalue()
), if the FixtureDef
's cached_value
is not already set, it calculates it by running the fixture function. Then it schedules a finalizer on the fixture request's node (request.node
) to clear the cached_value
.
In the example:
test_top
requestsfix
for the first time, which runs the fixture and schedules to clearfix
'scached_result
whenTestTop
is torn down.test_nested1
requestsfix
. At this point thefix
FixtureDef
hascached_value
set (sinceTestTop
's finalizer hasn't run yet), so it just uses it without rerunning the fixture. Then it schedules a finalizer to clearfix
whenTestNested1
is torn down.TestNested1
is torn down which clearsfix
.test_nested2
requestsfix
. Thecached_value
is cleared so it reruns the fixture.TestNested2
is torn down, clearsfix
.TestTop
is torn down, wants to clearcached_value
, but it is already cleared, goes 🤷 and continues.
Option 2 - Nested scope
Nested classes/packages get their own scope, nested within the parent class/package scope.
SETUP <Class TestTop>
top
SETUP <Class TestNested1>
nested1
TEARDOWN <Class TestNested1>
SETUP <Class TestNested2>
nested2
TEARDOWN <Class TestNested2>
TEARDOWN <Class TestTop>
Option 3 - Independent scope
Nested classes/packages get their own scope, independent of the parent class/package.
SETUP <Class TestTop>
top
TEARDOWN <Class TestTop>
SETUP <Class TestNested1>
nested1
TEARDOWN <Class TestNested1>
SETUP <Class TestNested2>
nested2
TEARDOWN <Class TestNested2>
Option 4 - Shared scope
Nested classes do not have a scope of their own; same scope as the top class/package.
SETUP <Class TestTop>
top
nested1
nested2
TEARDOWN <Class TestNested2>
My opinion
Option 1 seems broken to me, I don't think there's any case to be made for it.
Option 2 is my preference, I think it's the most coherent with the collection tree, lexical/filesystem layout and is pretty intuitive, for both packages and classes.
Option 3 I think is not intuitive, i.e. I think most would not expect fixtures to behave this way. It imposes some restrictions on tests ordering between the class and nested. Note that currently due to #7777, Packages work like this in practice.
Option 4 would be my second choice, it does make sense logically (that what if there is no test_top
?), but I think it's less intuitive than Option 2.
I can write more but don't want to make this too long.