LESSON 75 · DEEPER PYTHON32 min

Test Cases with unittest Fixtures and subTest

Use setUp for fresh test data and subTest to label multiple inputs within a unittest method.

Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons

Video lesson: Test Cases with unittest Fixtures and subTest

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    A passing test feels reassuring, but it only tells us that the assertions we actually wrote did not fail. If a test method contains pass, the test runner can be green while a function is plainly wrong. Today we will use a deliberately broken cart subtotal that returns the last price instead of the sum. We will run a false-green test, make it fail for the right reason, and then repair the calculation. Along the way, we will use `unittest.TestCase.setUp` to make fresh cases before each test method and `subTest` to label each input in a small table of variations. The final check will deliberately break the implementation again, proving that our

  2. 00:41

    assertions are capable of catching the regression. The companion PythonLessonLab lesson runs in a browser console and uses a TestSuite directly, so it does not need `unittest.main()` to inspect command-line arguments or exit the process. You can follow the exact code on screen. As we go, ask two questions: what behavior does each assertion protect, and would the test still pass if the function returned the wrong answer? The cart has prices two and three, so the subtotal should be five. The function returns three because it takes only the final element. Yet unittest reports success for one test method.

  3. 01:22

    The framework did run that method; the method simply made no claim about the behavior. This is why counting tests or seeing a green badge by itself cannot tell us whether important cases are protected. A test should contain an assertion that would fail for a plausible bug. For this function, the empty list and one- item list happen to work with the broken implementation. We need a multiple-item case to expose the wrong behavior. We also want fresh input data for each test method, so one test cannot leave a mutated list that affects another. That is what setUp is for. We will keep three cases together because they are variations of the same subtotal

  4. 02:02

    rule, and label each case with subTest. Then a failure report can identify which prices produced a mismatch instead of only pointing to one line inside an unlabelled loop. Now the suite is red, which is the correct outcome for this implementation. setUp prepares the case list before the test method runs. In unittest, each test method gets a fresh TestCase instance and its own setUp call, so a mutable fixture does not have to be shared across methods. The loop checks empty, one-item, and multiple-item carts. `with self.subTest(prices=prices)` labels each variation; if one assertion fails,

  5. 02:44

    unittest records that subtest and continues to later variations. The result shows one test method ran and one subtest failed. That count can seem surprising if you think each loop iteration is a whole new test method. It is not; subtests are variations inside one method. They are helpful when the setup and assertion are the same and only inputs vary. If an input needs a completely different setup or contract, a separate method may be clearer. The important diagnostic is that the multiple-item case exposes exactly the bug our first test missed. We should repair the function without weakening or deleting this assertion.

  6. 03:26

    The same tests now pass because sum handles all three selected cases. The empty list totals zero, the one-item list returns its sole value, and the multiple-item list returns five. We changed the implementation, not the expected values. That separation matters: if we adjusted the expected result from five to three just to make the test green, we would be documenting the bug as intended behavior. The test runner reports one method and no failures. The browser practice task uses an in- memory stream for the runner's usual textual report and prints a concise success line instead.

  7. 04:06

    That keeps its output deterministic while the assertions still execute. We have not proved every possible numeric input, and real money calculations might use Decimal rather than binary floating- point values. Those concerns are outside this short subtotal example. What we did establish is that the test covers the specific empty, single, and multiple-item behavior we promised. A well-designed fixture makes those cases easy to see, and subTest labels help when one case fails. The next step is to check that the green result is not another false comfort. This is a small regression probe. The correct function passes, and the deliberately wrong replacement fails

  8. 04:48

    against the same assertions. Restoring the original function afterward keeps the example clean. We do not need to ship monkeypatching like this in every test file, but it is a useful way to inspect whether a new test would actually detect a realistic mistake. In the companion exercise, the hidden checker performs a similar probe. A test method with only pass, or one that merely prints expected-looking values, will not satisfy it. Write the loop over self.cases, open a subTest for each price list, and call self.assertEqual on the real subtotal and expected value. If your suite prints True before you have written any assertion, remember the first

  9. 05:30

    broken run and do not mistake infrastructure success for behavior coverage. If a case fails, read the subtest label and compare the exact input with your expectation. Try adding one more variation after passing the task, such as repeated prices or a negative discount represented in the list. Decide what the function's contract says before adjusting the expected result. Tests are most useful when they can make a wrong implementation visibly fail.

Understand the concept

`unittest.TestCase` groups assertions into test methods. Its `setUp` method runs before each test method, so it is a good place to build fresh input data rather than reuse mutable state across tests. A test should fail when behavior is wrong, not depend on the order in which other tests happen to run. The `subtotal` example is deliberately small so the focus stays on the test structure.

A loop can check several cases in one test method, but a plain assertion reports only where execution stopped. `with self.subTest(prices=prices)` labels each case in the result and lets unittest continue to later cases after an assertion failure. The test runner still reports one test method, with one or more subtest failures. A subTest is not a replacement for distinct tests when the setup or expected behavior is fundamentally different; it is useful for variations of the same rule.

The browser exercise runs a TestSuite directly and writes runner details to an in-memory StringIO stream. `unittest.main()` normally inspects command-line arguments and may exit the process, which is not appropriate for an embedded browser task. The prepared program prints only `result.wasSuccessful()` and `result.testsRun`, giving deterministic output while the test checks still execute. The hidden checker also temporarily replaces subtotal with a wrong implementation, confirming that the written assertions would catch a regression.

The task supplies the production subtotal function and a list of three cases: an empty list, one item, and several items. Complete the test method by looping over the fixture, opening a subTest for each case, and comparing subtotal(prices) with expected. A good test suite passes for correct code and fails for wrong code. If you see a green result before writing any assertion, that is a warning: a test method containing only pass proves nothing.

  • setUp creates fresh data before every test method
  • subTest labels variations of one rule
  • A green test with no assertions proves nothing
  • A TestSuite can run without unittest.main or process exit

See it step by step

Read the code, predict the output, then compare it with the result.

01. A fixture is recreated for each method

PYTHON
import unittest
class FreshTests(unittest.TestCase):
    def setUp(self):
        self.items = []
    def test_add(self):
        self.items.append("x")
        self.assertEqual(self.items, ["x"])
    def test_starts_empty(self):
        self.assertEqual(self.items, [])
suite = unittest.defaultTestLoader.loadTestsFromTestCase(FreshTests)
result = unittest.TestResult()
suite.run(result)
print(result.wasSuccessful(), result.testsRun)
EXPECTED OUTPUT
True 2

Each test method receives a new TestCase instance and setUp builds a fresh list.

02. Label three cases with subTest

PYTHON
import unittest
class SquareTests(unittest.TestCase):
    def test_squares(self):
        for number, expected in ((0, 0), (2, 4), (-3, 9)):
            with self.subTest(number=number):
                self.assertEqual(number * number, expected)
result = unittest.TestResult()
unittest.defaultTestLoader.loadTestsFromTestCase(SquareTests).run(result)
print(result.wasSuccessful(), result.testsRun)
EXPECTED OUTPUT
True 1

One method checks three labeled variations of the same squaring rule.

03. A bad implementation makes the suite fail

PYTHON
import unittest
def broken_total(prices):
    return prices[-1] if prices else 0
class TotalTests(unittest.TestCase):
    def test_multiple(self):
        self.assertEqual(broken_total([2, 3]), 5)
result = unittest.TestResult()
unittest.defaultTestLoader.loadTestsFromTestCase(TotalTests).run(result)
print(result.wasSuccessful())
print(len(result.failures))
EXPECTED OUTPUT
False
1

A useful test turns red when a wrong function returns only the last item.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

Expose a green test that proves nothing

A test suite can be green even when the implementation is wrong. This deliberately broken subtotal function returns only the last item. The TestCase method contains pass and makes no assertion, so unittest reports success. The run proves that the test infrastructure executed, not that the function behaves correctly. Always check whether a test would turn red for a plausible wrong result.

This example runs a TestSuite with TestResult instead of invoking unittest.main. That keeps execution suitable for a browser console and gives deterministic output. Notice the side-by-side values: subtotal([2, 3]) prints three, while the empty test still reports success. The next step adds an assertion sensitive to the mistake.

PYTHON
import unittest

def broken_subtotal(prices):
    return prices[-1] if prices else 0

class EmptyTests(unittest.TestCase):
    def test_subtotal(self):
        pass

result = unittest.TestResult()
unittest.defaultTestLoader.loadTestsFromTestCase(EmptyTests).run(result)
print(broken_subtotal([2, 3]))
print(result.wasSuccessful(), result.testsRun)
EXPECTED OUTPUT
3
True 1
Follow the reasoning
  1. Compare the wrong subtotal with the expected five.
  2. Notice the green test result despite the missing assertion.
02 / 03

Build a fresh fixture and label each case

setUp runs before each test method and prepares the list of input-output pairs. The three cases cover an empty cart, one value, and multiple values. A loop keeps one rule in one method. Wrapping each iteration with `self.subTest(prices=prices)` records the input label and lets unittest continue checking other cases after a failure.

We intentionally run the test against the broken function here. The result is unsuccessful, and one subtest failure is recorded for [2, 3]. The test method count is still one; subtests are reported within it. This red result is useful because it shows the assertion can actually detect the known bug. The fixture list is created anew for this TestCase instance.

PYTHON
import unittest

def broken_subtotal(prices):
    return prices[-1] if prices else 0

class SubtotalTests(unittest.TestCase):
    def setUp(self):
        self.cases = [([], 0), ([2], 2), ([2, 3], 5)]
    def test_cases(self):
        for prices, expected in self.cases:
            with self.subTest(prices=prices):
                self.assertEqual(broken_subtotal(prices), expected)

result = unittest.TestResult()
unittest.defaultTestLoader.loadTestsFromTestCase(SubtotalTests).run(result)
print(result.wasSuccessful(), result.testsRun, len(result.failures))
EXPECTED OUTPUT
False 1 1
Follow the reasoning
  1. Point to the fresh case list constructed in setUp.
  2. Read the one failing subtest for the multiple-item input.
03 / 03

Verify the repaired function and a regression probe

Change subtotal to sum(prices), keeping the test cases unchanged. A correct implementation now passes all three subtests. Avoid changing both the implementation and expected values at once; that would remove the independent check. The suite prints True and one test method, with no failures. The browser exercise uses the same pattern with an in-memory output stream.

Then deliberately replace subtotal with a wrong function and run the suite again. A useful test must turn red. The hidden exercise checker performs that regression probe, so a test method that merely prints values or contains pass cannot succeed. These three variations are not exhaustive, but they exercise the empty, single-item, and multi-item behavior that the guide promises.

PYTHON
import unittest

def subtotal(prices):
    return sum(prices)

class SubtotalTests(unittest.TestCase):
    def setUp(self):
        self.cases = [([], 0), ([2], 2), ([2, 3], 5)]
    def test_cases(self):
        for prices, expected in self.cases:
            with self.subTest(prices=prices):
                self.assertEqual(subtotal(prices), expected)

result = unittest.TestResult()
unittest.defaultTestLoader.loadTestsFromTestCase(SubtotalTests).run(result)
print(result.wasSuccessful(), result.testsRun, len(result.failures))
EXPECTED OUTPUT
True 1 0
Follow the reasoning
  1. Run the repaired sum against the same fixture and subtests.
  2. Temporarily break subtotal afterward to confirm these assertions are capable of failing.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnComplete SubtotalTests.test_cases. Loop over the fixture from setUp, use `with self.subTest(prices=prices)`, and assert subtotal(prices) equals expected for every pair. The supplied runner should print True 1. Your test must also fail when subtotal is replaced with a wrong implementation.
Need a hint?

Inside test_cases use `for prices, expected in self.cases:`. Nest `with self.subTest(prices=prices):` and call `self.assertEqual(subtotal(prices), expected)`. Do not replace the test method with pass or print-only checks.

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.When does unittest call setUp?

    02.Why wrap each parameter case in subTest?

    03.What does a passing test method with only pass prove?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    A test that never asserts

    COMMON MISTAKE
    def test_cases(self):
        pass
    FIX
    with self.subTest(prices=prices):
        self.assertEqual(subtotal(prices), expected)

    What happens: The suite reports success even for a broken subtotal.
    Assertions make the test sensitive to wrong results.

    Sharing a mutable fixture across methods

    COMMON MISTAKE
    cases = []  # module-level list reused by tests
    FIX
    def setUp(self):
        self.cases = [([], 0), ([2], 2)]

    What happens: One test can leave state that changes another test.
    Construct fresh test data before each method.

    Browse this lesson’s problem fixes

    Sources and further reading