pytest-dev / pytest

The pytest framework makes it easy to write small tests, yet scales to support complex functional testing
https://pytest.org
MIT License
12.13k stars 2.69k forks source link

'class' scope fixture invoke and teardown order issue in nested test class #5148

Open megachweng opened 5 years ago

megachweng commented 5 years ago

Details

fixture name invoked and teardown every subclass method as I expect.

class TestClassTop:
    @pytest.fixture(scope='class')
    def name(self):
        print('   <<<< Invoke Name')
        yield
        print('   <<<< tear down')

    # def test_method_sub(self, name):  <--  comment it out
    #     pass

    class TestSubClassA:
        def test_sub_cls_method(self, name):
            pass

    class TestSubClassB:
        def test_sub_cls_method(self, name):
            pass

>>>
test_cls.py::TestClassTop::TestSubClassA::test_sub_cls_method    <<<< Invoke Name
PASSED   <<<< tear down

test_cls.py::TestClassTop::TestSubClassB::test_sub_cls_method    <<<< Invoke Name
PASSED   <<<< tear down

but fixture name invoked in TestClassTop.test_method_sub but teardown in TestClassTop.TestSubClassA.test_sub_cls_method

class TestClassTop:
    @pytest.fixture(scope='class')
    def name(self):
        print('   <<<< Invoke Name')
        yield
        print('   <<<< tear down')

    def test_method_sub(self, name):  # <--  uncomment it
        pass

    class TestSubClassA:
        def test_sub_cls_method(self, name):
            pass

    class TestSubClassB:
        def test_sub_cls_method(self, name):
            pass
>>>
test_cls.py::TestClassTop::test_method_sub    <<<< Invoke Name
PASSED
test_cls.py::TestClassTop::TestSubClassA::test_sub_cls_method PASSED   <<<< tear down

test_cls.py::TestClassTop::TestSubClassB::test_sub_cls_method    <<<< Invoke Name
PASSED   <<<< tear down

And If I move TestClassTop.test_method_sub to the bottom, the result seems right.

class TestClassTop:
    @pytest.fixture(scope='class')
    def name(self):
        print('   <<<< Invoke Name')
        yield
        print('   <<<< tear down')

    class TestSubClassA:
        def test_sub_cls_method(self, name):
            pass

    class TestSubClassB:
        def test_sub_cls_method(self, name):
            pass

    def test_method_sub(self, name):
        pass
>>>
 test_cls.py::TestClassTop::TestSubClassA::test_sub_cls_method    <<<< Invoke Name
PASSED   <<<< tear down

test_cls.py::TestClassTop::TestSubClassB::test_sub_cls_method    <<<< Invoke Name
PASSED   <<<< tear down

test_cls.py::TestClassTop::test_method_sub    <<<< Invoke Name
PASSED   <<<< tear down

Environment

Python 3.6.8 Mac OS High Sierra 10.13.6

Package        Version 
-------------- --------
pytest         4.4.0   
atomicwrites   1.3.0   
attrs          19.1.0  
autopep8       1.4.4   
certifi        2019.3.9
chardet        3.0.4   
idna           2.8     
Jinja2         2.10.1  
MarkupSafe     1.1.1   
more-itertools 7.0.0   
peewee         3.9.4   
pip            18.1    
pluggy         0.9.0   
prettytable    0.7.2   
py             1.8.0   
pycodestyle    2.5.0   
PyJWT          1.7.1   
PyMySQL        0.9.3   
redis          3.2.1   
requests       2.21.0  
retrying       1.3.3   
setuptools     40.6.2  
six            1.12.0  
treker         0.0.2   
urllib3        1.24.1
asottile commented 5 years ago

I can confirm this behaviour, and I do agree it seems a little bit strange.

The "class" level fixture is being reused both for the test in the top level class as well as the first test in the nested class (but not the second one?)

This is also the first time I've seen nested test classes (!) I didn't even know that was a thing!

bluetech commented 1 year ago

I believe the problem lies in the code for request.node (FixtureRequest.node property). What it does for class scope is just look up the node tree until it finds a Class node, and returns it. However, because classes can be nested, this is not correct; it needs to find its own class.

Another collection node which can be nested is Package; that however does try to handle the nested aspect based on fixturedef.baseid. It currently does this incorrectly (ref #10993), but that's another matter. After fixing #10993 for packages, we should switch classes to use the same kind of code as package.

Test case:

import pytest

class TestTop:
    @pytest.fixture(scope='class')
    def fix_top(self, request):
        # Currently gives TestNested
        assert isinstance(request.node.obj, TestTop)

    class TestNested:
        def test_it(self, fix_top):
            assert False
bluetech commented 1 year ago

While the previous comment is correct, it is actually not the cause of the issue. The real cause is described in the technical note in #11205.