Testing and Refactoring Object-oriented Programs · practice
Test Several Cases
grade_letter is a staircase:
def grade_letter(fraction):
if fraction >= 0.9:
return "A"
if fraction >= 0.8:
return "B"
...
Five branches, and four places where the answer changes. Writing one test per case gives you five nearly identical
There is a shape that gets both.
A list of cases, one loop
def test_each_band_gets_its_letter():
cases = [
(1.0, "A"),
(0.9, "A"),
(0.85, "B"),
(0.6, "D"),
(0.0, "F"),
]
for fraction, expected in cases:
assert grade_letter(fraction) == expected
The arrangement is the
Chapter 3 used exactly this shape for data. It reads the same way here, and for the same reason: when things differ only in their values, make the values the data.
The failure that tells you nothing
Run that test against a broken grade_letter and the report says:
test_each_band_gets_its_letter failed
assert grade_letter(fraction) == expected
Which fraction? You cannot tell. The
Fundamentals I mentioned that an assertion can carry a message. In a loop, it stops being a nicety:
for fraction, expected in cases:
assert grade_letter(fraction) == expected, (
f"{fraction} should be {expected}, got {grade_letter(fraction)}"
)
Now the failure names the case:
0.85 should be B, got C
The rule generalizes: when one test covers several cases, the message has to say which case. Otherwise you have traded away the thing that made small tests useful.
A looping test over eight cases fails. The assertion has no message. What does the report tell you?
Choose cases at the edges
Five branches do not need fifty cases. They need the values where the answer changes, and one either side.
0.9 is the boundary between B and A. Both 0.9 and 0.89 are worth a row, because >= and > are one keystroke apart and only one of them is right. A case at 0.94 tells you almost nothing that 0.9 did not.
This is the same instinct as testing the correct answer and the wrong one. Cases earn their place by being able to fail on their own.
Also worth pinning: dividing by nothing
percentage has a branch that has nothing to do with grades:
def percentage(score, total):
if total == 0:
return 0.0
return score / total
Without that guard, an empty quiz would crash the report with
Task
Write test_scoring.py, covering quizapp.scoring.
Import grade_letter and percentage, then write three tests:
test_each_band_gets_its_letter: oneof (fraction, expected_letter)cases, one, one assertion carrying a message that names the case. Cover every letter, and include a boundary itself and a just below it (for example, 0.9and0.89).test_percentage_of_an_empty_quiz_is_zero:percentage(0, 0)returns0.0rather than raising.test_percentage_divides_the_score_by_the_total:percentage(2, 5)returns0.4.
The looping test needs at least six cases, and its assertion message must include the fraction that failed.