Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
megachweng opened this issue Apr 19, 2019 · 3 comments
Labels
topic: fixtures anything involving fixtures directly or indirectly

Comments

@megachweng
Copy link

megachweng commented Apr 19, 2019

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
Copy link
Member

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!

@Zac-HD Zac-HD added the topic: fixtures anything involving fixtures directly or indirectly label Jun 5, 2019
@bluetech
Copy link
Member

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
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: fixtures anything involving fixtures directly or indirectly
Projects
None yet
Development

No branches or pull requests

4 participants