1

If I run the following test case:

class DummyTestCase(TestCase):
    @classmethod
    def setUpClass(cls):
        cls.foo = 42
        super().setUpClass()

    def test_1(self):
        self.foo += 1
        print(f"=================> {self.foo}")

    def test_2(self):
        self.foo += 1
        print(f"=================> {self.foo}")

I get the following result:

tests/test_dummy.py::DummyTestCase::test_1 =================> 43
PASSED
tests/test_dummy.py::DummyTestCase::test_2 =================> 43
PASSED

How comes that I don't get 44 for the second test?

I understand that this would not be desirable for test isolation, but that would be what I would have expected if the test case behaved like a common class on which self.foo += 1 is called twice. What implementation details in unittest make it behave like that?

Weier
  • 1,339
  • 1
  • 10
  • 20

3 Answers3

2

A new TestCase instance is created before running each method, to achieve isolation between tests. This behavior is mentioned in the docs:

A new TestCase instance is created as a unique test fixture used to execute each individual test method.

The attribute you set in your setup code is a class attribute, and the assignment is done in a class method, setUpClass. This method is only called once, but in each test you set an instance attribute self.foo calculated from the value of the class attribute DummyTestCase.foo (which is set to 42 and never changed).

If you had used the setUp instance method instead, you'd get the same result, but under the hood the setUp method would be called twice, each time on a new instance, and set instance attribute self.foo to 42.

Here are some remarks about instance and class attributes from the Python docs.

Lev Levitsky
  • 63,701
  • 20
  • 147
  • 175
  • this contradicts the docs for `setUpClass` though https://docs.python.org/3/library/unittest.html#class-and-module-fixtures _"This will lead to setUpClass / setUpModule (etc) being called exactly once per class and module"_ – Anentropic Jun 08 '22 at 08:57
  • @Anentropic both statements are true. `setUpClass` is a `@classmethod` so when it's called it changes the value of the `foo` for all existing instances. – Marco Bonelli Jun 08 '22 at 09:08
  • I tried to provide more context about this in the answer, hope that helps. – Lev Levitsky Jun 08 '22 at 09:15
2

In short, unittest simply instantiates the class before every test. The .setUpClass() class method modifies the class attribute foo, which is then read by any instance that did not create its own instance attribute foo. See also Difference between Class Attributes, Instance Attributes, and Instance Methods in Python.

The fact that unittest provides you with a base TestCase class to subclass is just for convenience. This is so you have access to everything you need on self and you can create multiple classes (one for each test category) with multiple methods (one for each test in the category). Without classes you would have to awkwardly manage a bunch of global functions or split different test categories into different modules. This class approach also allows to easily select and filter which test classes to run from command line.

See Persist variable changes between tests in unittest? for a method to keep a "state" between tests in the same test case class.

This example is exactly what happens in your case and could help clarify the confusion regarding class vs instance attributes:

class C:
    @classmethod
    def setUpClass(cls):
        cls.foo = 1

    def test_1(self):
        print(self.foo)
        self.foo = 2

    def test_2(self):
        print(self.foo)

c1 = C()
c2 = C()
C.setUpClass()
c1.test_1() # prints class attribute, then creates instance attribute
c2.test_2() # prints class attribute
c1.test_1() # prints instance attribute now

Output will be:

1
1
2
Marco Bonelli
  • 63,369
  • 21
  • 118
  • 128
  • OK so basically one `TestCase` will be instanciated per test method, but for convenience we define multiple test methods on the same `TestCase` subclass. This is a bit tricky. – Weier Jun 08 '22 at 09:38
  • @Weier no. `TestCase` is instantiated once per test method. It's just that it doesn't matter because when `DummyTestCase.setUpClass()` is called it will set the *class attribute* `foo` and all the instances will see the value of `foo` as `42` since they didn't create an *instance attribute* `foo` theirselves. – Marco Bonelli Jun 08 '22 at 13:39
  • In my piece of code, `self.foo += 1` also creates an instance attribute that would be incremented twice (`test_1` and `test_2`) if each test wasn't run in a dedicated instance. – Weier Jun 08 '22 at 15:03
  • @Weier yep that's true. If the class was only instantiated once you would see the second test print `43`. – Marco Bonelli Jun 08 '22 at 19:10
0

The implementation of this behavior is in suite.py line 114

if _isnotsuite(test):
    self._tearDownPreviousClass(test, result)
    self._handleModuleFixture(test, result)
    self._handleClassSetUp(test, result)
    result._previousTestClass = test.__class__

    if (getattr(test.__class__, '_classSetupFailed', False) or
        getattr(result, '_moduleSetUpFailed', False)):
            continue

_handleClassSetUp is calling setUpClass if it is not None:

if setUpClass is not None:
    _call_if_exists(result, '_setupStdout')
    try:
        try:
            setUpClass()
        except:
            ...

As you can see, setUpClass is called for each test class in the test suite.

Jacques Gaudin
  • 15,779
  • 10
  • 54
  • 75
  • 1
    To be more precise, it's called once per test class (not once per test method): there is a [check](https://github.com/python/cpython/blob/6b9122483f1f26afb0c41bd676f9754ffe726e18/Lib/unittest/suite.py#L145) in `_handleClassSetUp` that short-circuits if the test class is the same as before. Right after calling `_handleClassSetUp` once, [`_previousTestClass` is set](https://github.com/python/cpython/blob/6b9122483f1f26afb0c41bd676f9754ffe726e18/Lib/unittest/suite.py#L115), so `setUpClass` will not be called again for the same class. – Lev Levitsky Jun 08 '22 at 09:25