Learning Goals
3 min- Write a test case with all its parts (id, steps, expected, actual).
- Use equivalence partitioning to group inputs.
- Use boundary value analysis to test the edges.
- Cover happy paths AND error paths systematically.
Warm-Up · You Can't Test Everything
5 minA function takes an age (0-150). That's 151 valid values plus infinite invalid ones (-5, 1000, "abc", 3.7...). You can't test them all. But you don't need to — most behave identically. Test one representative from each group, plus the edges.
Smart test selection beats exhaustive testing. Group inputs that the code treats the same (equivalence partitions), test one from each group, then hammer the boundaries between groups — because that's where off-by-one bugs live.
New Concept · Partitions, Boundaries, Cases
14 minAnatomy of a test case
ID: TC-007
Title: Reject under-age signup
Pre: signup form open
Steps: 1. enter age = 12
2. submit
Expected: "must be 13 or older" error; not registered
Actual: (filled in when you run it)
Status: pass / failEquivalence partitioning
Split inputs into groups the code treats identically. For "age must be 13-120":
partition example expected too low (<13) 7 reject valid (13-120) 30 accept too high (>120) 200 reject not a number "abc" reject Test ONE from each partition → 4 tests cover the whole input space.
Boundary value analysis
Bugs cluster at the edges of partitions (the classic < vs <= mistake). Test just inside, on, and just outside each boundary:
boundary at 13: 12 (reject), 13 (accept), 14 (accept) boundary at 120: 119 (accept), 120 (accept), 121 (reject)
Combine them
def can_signup(age): return 13 <= age <= 120 # one per partition + the boundaries assert can_signup(7) is False # too low assert can_signup(30) is True # valid assert can_signup(200) is False # too high # boundaries — where off-by-one bugs hide assert can_signup(12) is False assert can_signup(13) is True assert can_signup(120) is True assert can_signup(121) is False print("all boundary checks passed ✅")
Seven well-chosen tests prove the function across its entire input space — far better than 50 random ones.
Worked Example · Test-Case Table for a Discount
12 minSpec: orders over RM100 get 10% off; orders over RM500 get 20% off; otherwise no discount.
Partitions: 0-100 (none), 100.01-500 (10%), 500.01+ (20%) Boundaries: 100, 500 (and the values just around them) ID input expected pay notes TC1 50 50.00 none partition TC2 100 100.00 boundary — "over 100" excludes 100 TC3 100.01 90.01 just into 10% partition TC4 300 270.00 mid 10% partition TC5 500 450.00 boundary — "over 500" excludes 500 → still 10% TC6 500.01 400.01 just into 20% partition TC7 1000 800.00 mid 20% partition
def total_due(amount): if amount > 500: return amount * 0.8 if amount > 100: return amount * 0.9 return amount cases = [(50, 50), (100, 100), (100.01, 90.009), (300, 270), (500, 450), (500.01, 400.008), (1000, 800)] for amount, expected in cases: got = round(total_due(amount), 3) assert got == round(expected, 3), f"{amount}: got {got}, want {expected}" print("all 7 cases passed ✅")
Read the diff
The boundary cases TC2 and TC5 are the gold — they pin down whether the rule is "over 100" (strict >) or "100 or more" (>=). That single ambiguity causes endless real-world bugs; boundary testing forces you to decide and lock it in.
Try It Yourself
13 minFor "a password must be 8-64 characters", list the equivalence partitions and the boundary values to test.
Answer
Partitions: too short (<8), valid (8-64), too long (>64). Boundaries: 7, 8, 9 and 63, 64, 65.
For a grading function (A≥80, B≥65, C≥50, F<50), build a test-case table with partitions + boundaries. Then assert them in code.
Extend your grade table with invalid inputs: negative score, score > 100, a string. What should each do? Add cases.
Mini-Challenge · Test a Date Validator
8 minDesign a test-case table for a function is_valid_date(day, month, year). Use partitions + boundaries to cover: valid dates, day 0 / 32, month 0 / 13, February 29 in leap and non-leap years. Then implement enough of the function to pass them.
Show the case ideas
valid: (15, 6, 2024) → True day boundary: (0, 6, 2024) → False; (31, 6, 2024) → False (June has 30) month boundary:(15, 0, 2024) → False; (15, 13, 2024) → False leap year: (29, 2, 2024) → True; (29, 2, 2023) → False month-end: (31, 1, 2024) → True; (31, 4, 2024) → False (April=30)
Non-negotiables: cover day/month boundaries AND the February-leap-year edge — the most bug-prone date logic of all.
Recap
3 minYou can't test everything, so test smart: split inputs into equivalence partitions (groups treated the same), test one from each, then hammer the boundaries where off-by-one bugs live. A test case has an id, steps, expected and actual results. Design cases on paper first — it clarifies the spec before you write code. Next: turning these into real assert tests.
Vocabulary Card
- test case
- A documented input + expected result for a specific scenario.
- equivalence partition
- A group of inputs the code treats identically; test one representative.
- boundary value
- A value at the edge of a partition — where off-by-one bugs cluster.
- happy / error path
- Valid-input scenarios / invalid-input scenarios.
Homework
4 minPick a function with clear input rules (a fare calculator, a BMI banding, a password strength checker). Write a full test-case table using partitions + boundaries, then turn every row into an assert. Aim for the minimum cases that prove correctness across the whole input space.
Follow the discount worked example for your chosen function. The grader will look for boundary cases — "just below / on / just above" each threshold.