python: fix instance handling in static and class method tests
and also fixes a regression in pytest 8.0.0 where `setup_method` crashes if the class has static or class method tests. It is allowed to have a test class with static/class methods which request non-static/class method fixtures (including `setup_method` xunit-fixture). I take it as a given that we need to support this somewhat odd scenario (stdlib unittest also supports it). This raises a question -- when a staticmethod test requests a bound fixture, what is that fixture's `self`? stdlib unittest says - a fresh instance for the test. Previously, pytest said - some instance that is shared by all static/class methods. This is definitely broken since it breaks test isolation. Change pytest to behave like stdlib unittest here. In practice, this means stopping to rely on `self.obj.__self__` to get to the instance from the test function's binding. This doesn't work because staticmethods are not bound to anything. Instead, keep the instance explicitly and use that. BTW, I think this will allow us to change `Class`'s fixture collection (`parsefactories`) to happen on the class itself instead of a class instance, allowing us to avoid one class instantiation. But needs more work. Fixes #12065.
This commit is contained in:
@@ -4577,3 +4577,48 @@ def test_deduplicate_names() -> None:
|
||||
assert items == ("a", "b", "c", "d")
|
||||
items = deduplicate_names((*items, "g", "f", "g", "e", "b"))
|
||||
assert items == ("a", "b", "c", "d", "g", "f", "e")
|
||||
|
||||
|
||||
def test_staticmethod_classmethod_fixture_instance(pytester: Pytester) -> None:
|
||||
"""Ensure that static and class methods get and have access to a fresh
|
||||
instance.
|
||||
|
||||
This also ensures `setup_method` works well with static and class methods.
|
||||
|
||||
Regression test for #12065.
|
||||
"""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
|
||||
class Test:
|
||||
ran_setup_method = False
|
||||
ran_fixture = False
|
||||
|
||||
def setup_method(self):
|
||||
assert not self.ran_setup_method
|
||||
self.ran_setup_method = True
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fixture(self):
|
||||
assert not self.ran_fixture
|
||||
self.ran_fixture = True
|
||||
|
||||
def test_method(self):
|
||||
assert self.ran_setup_method
|
||||
assert self.ran_fixture
|
||||
|
||||
@staticmethod
|
||||
def test_1(request):
|
||||
assert request.instance.ran_setup_method
|
||||
assert request.instance.ran_fixture
|
||||
|
||||
@classmethod
|
||||
def test_2(cls, request):
|
||||
assert request.instance.ran_setup_method
|
||||
assert request.instance.ran_fixture
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == ExitCode.OK
|
||||
result.assert_outcomes(passed=3)
|
||||
|
||||
@@ -410,22 +410,37 @@ def test_function_instance(pytester: Pytester) -> None:
|
||||
items = pytester.getitems(
|
||||
"""
|
||||
def test_func(): pass
|
||||
|
||||
class TestIt:
|
||||
def test_method(self): pass
|
||||
|
||||
@classmethod
|
||||
def test_class(cls): pass
|
||||
|
||||
@staticmethod
|
||||
def test_static(): pass
|
||||
"""
|
||||
)
|
||||
assert len(items) == 4
|
||||
|
||||
assert isinstance(items[0], Function)
|
||||
assert items[0].name == "test_func"
|
||||
assert items[0].instance is None
|
||||
|
||||
assert isinstance(items[1], Function)
|
||||
assert items[1].name == "test_method"
|
||||
assert items[1].instance is not None
|
||||
assert items[1].instance.__class__.__name__ == "TestIt"
|
||||
|
||||
# Even class and static methods get an instance!
|
||||
# This is the instance used for bound fixture methods, which
|
||||
# class/staticmethod tests are perfectly able to request.
|
||||
assert isinstance(items[2], Function)
|
||||
assert items[2].name == "test_class"
|
||||
assert items[2].instance is not None
|
||||
|
||||
assert isinstance(items[3], Function)
|
||||
assert items[3].name == "test_static"
|
||||
assert items[3].instance is None
|
||||
assert items[3].instance is not None
|
||||
|
||||
assert items[1].instance is not items[2].instance is not items[3].instance
|
||||
|
||||
Reference in New Issue
Block a user