runtime
123 / 250
lesson 123 Testing & Code Quality · intermediate

One test, a table of cases

Many inputs, one rule: put the inputs and expected results in a table and loop over it. subTest makes unittest report every failing row instead of stopping at the first.

You'll be able to
  • Write a table of input and expected-result pairs and check them in one loop
  • Use self.subTest(...) so each failing case is reported with its own values
  • Choose the rows of a table to cover each band and each boundary of a rule
The idea

Postage depends on weight bands: up to 100 g costs 85p, up to 250 g costs 135p, up to 750 g costs 210p, and heavier letters aren't accepted. Testing that rule needs many inputs: one inside each band and one on each boundary. Writing a test method for each is repetitive. A table is clearer: a list of (input, expected) pairs, checked in one loop. Adding a case means adding a row.

A plain loop has a weakness: the first failing assert stops the test, so you see one wrong row even if three are wrong.

Run each example to see what it prints. Running examples doesn't count toward progress.

example-1.py · A table in a plain loop
def postage(weight_g):
    if weight_g < 100:
        return 85
    if weight_g < 250:
        return 135
    if weight_g <= 750:
        return 210
    raise ValueError(f"{weight_g} g is too heavy for a letter")

CASES = [(20, 85), (100, 85), (101, 135), (250, 135), (750, 210)]

def test_price_table():
    for weight, pence in CASES:
        assert postage(weight) == pence, f"{weight} g: expected {pence}, got {postage(weight)}"

try:
    test_price_table()
    print("PASS")
except AssertionError as err:
    print("FAIL", err)

unittest's with self.subTest(name=value): marks one pass of the loop as a sub-test. A failure inside it is reported with the values you passed, and the loop carries on with the next row. pytest's equivalent is @pytest.mark.parametrize, which turns each row into its own test.

example-2.py · The same table with subTest
import unittest

def postage(weight_g):
    if weight_g < 100:
        return 85
    if weight_g < 250:
        return 135
    if weight_g <= 750:
        return 210
    raise ValueError(f"{weight_g} g is too heavy for a letter")

CASES = [(20, 85), (100, 85), (101, 135), (250, 135), (750, 210)]

class TestPostage(unittest.TestCase):
    def test_price_table(self):
        for weight, pence in CASES:
            with self.subTest(weight=weight):
                self.assertEqual(postage(weight), pence)

    def test_too_heavy(self):
        with self.assertRaises(ValueError):
            postage(751)

def run(case):
    result = unittest.TestResult()
    unittest.defaultTestLoader.loadTestsFromTestCase(case).run(result)
    print("ran", result.testsRun, "| failures", len(result.failures), "| errors", len(result.errors))
    for test, details in result.failures + result.errors:
        print("  " + test.id().split(".", 1)[1], "->", details.strip().splitlines()[-1])

run(TestPostage)

When to use it: whenever one rule should hold for many inputs: price bands, validation rules, parsing formats. Choose rows deliberately: one inside each band, and both sides of every boundary (100 and 101, 250 and 251).

Mistakes to avoid: a table with only comfortable values in the middle of each band (it can't catch < versus <=); putting the assert outside the with self.subTest(...) block (the failure isn't attached to the row); and computing the expected values with the code under test, which makes the test agree with any bug.

Review
  • A table of (input, expected) rows checks one rule on many inputs in one loop.
  • with self.subTest(...) reports every failing row with its values, instead of stopping at the first.
  • Pick rows inside each band and on both sides of every boundary; never compute expected values with the code under test.
  • Next: fresh test data for every test, and stand-ins for parts you don't want a test to call.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

One test, a table of cases — Testing & Code Quality · Runtime