Batu Lab NotesPractical developer guides

Why does a unittest fixture retain data between tests?

By Batu ยท English technical notes

Also published in our primary archive.

A unittest fixture retains the append because the list is a class attribute, so every test instance reaches the same mutable object. unittest creates a distinct TestCase instance for each test method, but that does not copy class attributes. Put the list creation in setUp() when each test needs independent state.

This experiment deliberately runs test_a_append before test_b_starts_empty. SharedListTests.items begins as one list. The first test appends "one"; the second instance then observes that same list and fails. The failure count is therefore one, and the remaining class-level contents make the leak visible.

FreshListTests moves self.items = [] into setUp(). Each test method gets a fresh instance and setUp() assigns a fresh list to that instance. Both tests pass. The final assertions check the two results rather than relying on a runner's presentation, whose formatting can vary. The corrected fixture recreates the mutable list itself; merely creating another test case would not repair a class attribute shared by those cases.

This does not mean every class attribute is wrong: immutable constants are often appropriate there. It specifically addresses mutable state whose contents are changed by a test. Class fixtures have a different lifetime and need a separate design choice. Python's documentation states that setUp() is called immediately before each test method and that each test method has a unique TestCase instance (setUp).

AI assistance disclosure: this article and its synthetic example were prepared with AI assistance.

Example

import unittest


class SharedListTests(unittest.TestCase):
    items = []

    def test_a_append(self):
        self.items.append("one")
        self.assertEqual(self.items, ["one"])

    def test_b_starts_empty(self):
        self.assertEqual(self.items, [])


class FreshListTests(unittest.TestCase):
    def setUp(self):
        self.items = []

    def test_a_append(self):
        self.items.append("one")
        self.assertEqual(self.items, ["one"])

    def test_b_starts_empty(self):
        self.assertEqual(self.items, [])


def run(case):
    result = unittest.TestResult()
    unittest.defaultTestLoader.loadTestsFromTestCase(case).run(result)
    return len(result.failures)


shared_failures = run(SharedListTests)
fresh_failures = run(FreshListTests)
assert shared_failures == 1
assert SharedListTests.items == ["one"]
assert fresh_failures == 0
print(f"shared failures: {shared_failures}")
print(f"shared contents: {SharedListTests.items}")
print(f"fresh failures: {fresh_failures}")

Expected output:

shared failures: 1
shared contents: ['one']
fresh failures: 0