Unit 14 · lesson
Turn the Bug Into a Test
Core path: 30 minutes
A bug report can disappear into memory.
A regression test can stay in the project and keep asking the same question every time the code changes.
That is one of the best upgrades you can make to a debugging workflow:
once a bug teaches you something about the intended behavior, preserve that lesson as a test.
Start with a real failure
Suppose this function exists:
def is_low_battery(level):
return level < 20
The requirement says:
20% counts as low battery.
21% does not.
The bug is at the boundary.
Before fixing it, write:
def test_low_battery_boundary():
assert is_low_battery(20) is True
assert is_low_battery(21) is False
Run the test.
The first assertion should fail against the buggy implementation.
Now repair:
def is_low_battery(level):
return level <= 20
Run the test again.
The bug now has a permanent witness.
Why boundary pairs are powerful
A single test:
assert is_low_battery(10) is True
does not tell you whether the exact boundary is correct.
The pair:
20 → True
21 → False
sits directly on opposite sides of the requirement.
This is the same boundary reasoning from Week 4, now encoded as automated verification.
The course is starting to fold back on itself on purpose.
A regression test is history in executable form
Imagine the bug is fixed today.
Three weeks later somebody refactors the function and accidentally changes <= back to <.
Without the test, the old bug may quietly return.
With the test:
python -m pytest
reports the broken contract immediately.
The test is evidence of a requirement the team previously learned the hard way.
Do not write a test that copies the bug
Suppose the buggy function returns the wrong value and you write a test that expects that wrong value merely because "that is what the function currently does."
You have automated the defect.
A test needs a source of expected behavior:
- a requirement;
- a specification;
- an agreed example;
- a documented invariant;
- a known boundary rule.
Tests are not automatically correct because they live in test_*.py.
A failing test can be better than no test
During development, red is not shame.
A deliberately failing test can mean:
I have described the behavior I need.
The implementation does not satisfy it yet.
That is useful state.
The dangerous version is a failing test nobody understands, or a test somebody deletes just to make the suite green.
Green without the requirement is theater.
Convert one old bug from the course
Choose a bug you encountered earlier:
- Week 4 branch boundary;
- Week 5 off-by-one loop;
- Week 6 status function;
- Week 7 console bug;
- Week 9 persistence helper;
- another approved example.
Write a small function-level regression test that would fail if the old bug returned.
Document:
original bug:
requirement:
input that exposes it:
expected result:
why this test would catch the regression:
Then deliberately reintroduce the bug, run the test, and capture the failure.
Restore the correct implementation and capture the passing result.
Tests are evidence, not omniscience
A suite can be green while an untested behavior is broken.
So when somebody says:
All tests pass.
The next useful question is:
Which claims do those tests actually cover?
That is not cynicism. It is precision.
A test suite is a map of verified expectations. The blank spaces on the map still exist.
Reader workbench
Make the boundary executable
This is the Unit's one-file practice surface. Read the code, predict one result, run it, then change a value, input, condition, or boundary and explain why the evidence changed. Multi-file projects, Git, terminals, packages, and live services still belong in the full development workspace.
Run the code to see output.
This runs in an isolated Python worker in your browser. Your edits stay in this browser until you reset them.