pytest: TestCase.setUpClass is not called in some cases in pytest>=6.2.0

  • a detailed description of the bug or problem you are having
  • output of pip list from the virtual environment you are using
  • pytest and operating system versions: pytest>=6.2.0,<=6.2.2
  • minimal example if possible
python version: 3.8.7 (v3.8.7:6503f05dd5, Dec 21 2020, 12:45:15)  [Clang 6.0 (clang-600.0.57)]
platform: macOS-10.16-x86_64-i386-64bit

The problem

pytest doesn’t call setUpClass, if test method has / in its name. Test method itself is executed.

In example below setUpClass is executed only after running first test method. If there wasn’t another test method without / in its name, setUpClass wouldn’t be called at all.

from unittest import TestCase

class BaseTest(TestCase):

    def __init_subclass__(cls, **kwargs):
        def test_method(self):
            self.assertEqual('foo', self.setup_class_attr)

        # / symbol on the next line causes the problem
        setattr(cls, "test_setup_class_attr['/foo']", test_method)
        setattr(cls, "test_setup_class_attr['foo']", test_method)

    @classmethod
    def setUpClass(cls):
        cls.setup_class_attr = 'foo'

class Test(BaseTest):
    ...

Running with unittests (gives expected results):

Ran 2 tests in 0.005s

OK

Running with pytest<6.2.0 (gives expected results):

$ pytest
====================================== test session starts ======================================
platform darwin -- Python 3.8.7, pytest-6.1.2, py-1.10.0, pluggy-0.13.1
rootdir: /Users/rocky/py/test
collected 2 items                                                                               

test_setup_cls.py ..                                                                      [100%]

======================================= 2 passed in 0.04s =======================================

Running with pytest >=6.2.0:

$ pytest
====================================== test session starts =======================================
platform darwin -- Python 3.8.7, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
rootdir: /Users/rocky/py/test
collected 2 items                                                                                

test_setup_cls.py F.                                                                       [100%]

============================================ FAILURES ============================================
_______________________________ Test.test_setup_class_attr['/foo'] _______________________________

self = <test_setup_cls.Test testMethod=test_setup_class_attr['/foo']>

    def test_method(self):
>       self.assertEqual('foo', self.setup_class_attr)
E       AttributeError: 'Test' object has no attribute 'setup_class_attr'

test_setup_cls.py:7: AttributeError
==================================== short test summary info =====================================
FAILED test_setup_cls.py::Test::test_setup_class_attr['/foo'] - AttributeError: 'Test' object h...
================================== 1 failed, 1 passed in 0.20s ===================================
$ pip list          
Package    Version
---------- -------
attrs      20.3.0
iniconfig  1.1.1
packaging  20.9
pip        21.0.1
pluggy     0.13.1
py         1.10.0
pyparsing  2.4.7
pytest     6.2.2
setuptools 54.2.0
toml       0.10.2

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Comments: 17 (17 by maintainers)

Commits related to this issue

Most upvoted comments

@parthdpa Sure! I’ll try to write some things to get you started.

Nodeids

The pytest collection phase creates a tree of nodes. If you have a test suite, you can see the tree by running pytest --collect-only.

Each node has a parent (exception Session, the root node) and nodeid. The nodeid of some test might look like this:

testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source

This is the nodeid of test test_repr_source in test class TestFormattedExcinfo in file testing/code/test_excinfo.py.

The parent of this node is the node with nodeid

testing/code/test_excinfo.py::TestFormattedExcinfo

which represents the test class, whose parent in turn is testing/code/test_excinfo.py, whose parent is testing/code and so on, until we reach Session whose nodeid is the empty string "" (without the quotes).

Generally the path parts are separated by / and the further parts are separated by ::.

Nodeid parents

Some code wants to know whether one nodeid is the parent of another nodeid. For example, if testing/code defines an autouse=True fixture, then it should affect its children nodes, but not others.

Previously this was done with an ischildnode method, but this turned out to be slow in some cases, so commit aa0e2d654fb0c8ac18747fe1cdf54d8f29bcd24a changed it such that a iterparentnodeids function computes all of the parent nodeids of a given node, which is then faster to check for containment.

New code

The code for iterparentnodeids uses the following rule: “Note that :: parts are only considered at the last / component.”. I.e.

testing/code/test_excinfo.py::TestFormattedExcinfo
            ^ last /

so in the case of

testing::foo/code/test_excinfo.py::TestFormattedExcinfo

The path part is still testing::foo/code/test_excinfo.py.

Problem

The problem is that it broke @MrMrRobat case:

x.py::Test::test_setup_class_attr[/foo]

The path part here is taken according to the rule above as x.py::Test::test_setup_class_attr[/ (up to the first /). But what was really intended is for the path part to be just x.py.

Solution

Well that’s where you come in 😃

Here are some possible solutions from the top of my head:

  1. Keep the current behavior. This is necessary if we want to allow :: in paths.
  2. Change the rule to / parts are only considered at the first :: component, i.e. give precedence to ::. This will break :: in paths which may be fine.
  3. Use a rule which considers [] brackets in some special way, as @RonnyPfannschmidt suggests.
  4. Use some dynamic rule which takes in consideration the actual existing nodes instead of just the static nodeid format.

Personally I wouldn’t want to do 4, I think static meaning is very good to have.

I would try 2 and see if it breaks anything, maybe that would be enough.

Method with / in the name, I guess in pytest you do see everything 😀

Thanks for the bisect @nicoddemus, I’ll take a look soon.

@bluetech a possible initial quick-fix could be to split them out of parameter names (via ‘[’)