Tables and properties · Software Testing
How do you test many cases without writing many tests?
Course contentsWhy you need it
Some functions need dozens of cases: every price band, every date format. Writing a function for each is tiring and hides the pattern. Two techniques scale much better: tables of inputs and expected outputs, and properties that must hold for any input, checked on hundreds of random ones.
The intuition
Table-driven tests: one loop, many rows.
CASES = [(1, 5), (1.01, 9), (5, 9), (6, 11)]
for weight, want in CASES:
assert shipping(weight) == want, (weight, shipping(weight))In pytest: @pytest.mark.parametrize("weight, want", CASES), which reports each row as its own test.
Property-based tests: sometimes you can't easily say the exact answer, but you know rules it must follow. For any list, a sort function's output must:
- have the same length,
- contain the same items (same counts),
- be in order.
Generate many random inputs, including awkward ones (empty, duplicates, negatives), and check the rules on each. Use a fixed seed (random.Random(1)) so a failure can be reproduced. The Hypothesis library does this for Python and even shrinks a failing input to the smallest one.
Definition
Table-driven test
One test that loops over rows of (input, expected).
Property-based test
A test that checks rules that must hold for any input, on many generated inputs.
Seed
The starting value of a random generator. The same seed gives the same "random" numbers, so failures can be repeated.
Examples
A table of cases
Try it yourselfEvery boundary of the price bands in one list.
import math
def shipping(weight_kg):
if weight_kg <= 1:
return 5
if weight_kg <= 5:
return 9
return 9 + 2 * math.ceil(weight_kg - 5)
CASES = [(0.5, 5), (1, 5), (1.01, 9), (5, 9), (5.5, 11), (7, 13)]
failures = 0
for weight, want in CASES:
got = shipping(weight)
if got != want:
failures += 1
print("FAIL", weight, "want", want, "got", got)
print(len(CASES), "cases,", failures, "failures")6 cases, 0 failures
- L10Each row is one test: the boundaries 1 and 5, and just past them.
- L12One loop runs them all and reports each failure with its inputs.
Checking properties
Try it yourselfRun a sort on 300 random lists and check the three rules on each.
import random
from collections import Counter
def looks_sorted(fn):
rng = random.Random(1)
for _ in range(300):
xs = [rng.randint(-3, 3) for _ in range(rng.randint(0, 6))]
out = fn(list(xs))
if len(out) != len(xs) or Counter(out) != Counter(xs):
return False
if any(out[i] > out[i + 1] for i in range(len(out) - 1)):
return False
return True
print(looks_sorted(sorted))
print(looks_sorted(lambda xs: sorted(set(xs))))
print(looks_sorted(lambda xs: sorted(xs, reverse=True)))True False False
- L5A fixed seed: the same lists every run.
- L7A small range of values makes duplicates likely, and includes negatives.
- L9Same items with the same counts.
- L16Drops duplicates: caught by the count rule.
Common mistakes
Random tests you can't repeat
If a random test fails once and you can't get the same input again, you can't debug it. Use a seeded generator.
Re-implementing the code in the test
A test that computes the answer the same way as the code repeats the same bug. Use known values or independent properties.
Only nice random data
Random lists of distinct positive numbers never test duplicates, negatives or empty lists. Shape the generator to include them.
Quick quiz
1. Which is a property of a correct `reverse(xs)` that you can check on any list?
Show the answer
Answer: A
Reversing twice gives back the original, for every list. (reverse(xs) == xs is fine for [1, 2, 1].)
2. Why seed the random generator in a property test?
Show the answer
Answer: A
Same seed, same inputs: you can rerun the failing case and debug it.
Practice
3 exercises in three steps: predict what a program prints, change a program to meet a new requirement, then write one from scratch. Your code runs in the browser and is checked the moment you submit.
- PredictRun the table
Type exactly what this prints.
- ModifyKeep the order
dedupe(items)should remove repeats and keep the first occurrence of each item in its original order. The property checkdedupe(xs)keeps order fails, because a set doesn't keep order. Fix it. - WriteA property checker
Write
check_sort(fn)that returnsTrueiffnbehaves like a correct sort on 200 random lists, andFalseas soon as one breaks a rule:- same length as the input
- the same items with the same counts
- in order
The grader tries it on
sortedand on broken sorts that drop duplicates, reverse, change negative numbers or sort only part of the list. Userandom.Random(seed), and make lists that include negatives, duplicates, empty lists and lists longer than 6.
Practise this lesson
Open the interactive lesson to edit and run these examples and solve the exercises with instant feedback. Free to start, no sign-up.
Interactive lessons open in order. If you have not done the earlier ones yet, you start at the course map.
Terms in this lesson
| Term | Chinese | Definition |
|---|---|---|
| Table-driven test | 表格驱动测试 | Many (input, expected) rows checked in one loop. |
| Property-based test | 基于性质的测试 | Rules checked on many generated inputs. |
| Hypothesis | Hypothesis | A Python library for property-based testing. |