Recipe
Fix a reported bug, test first
Use when: someone reports a wrong result ("an empty message counts as one word") in code that has, or can have, automated tests
Goal: a fix, plus a regression test that you've seen fail on the old code and pass on the new code
Taught in Test-first bug fixing · extends Debug a crash or a wrong answer
Quick recipe
-
1
Before touching the code, write one check per distinct cause in the report. Work out each expected value from what the code is supposed to do, by hand. Never compute it by calling the code under test.
Example
The report: an empty message counts as one word, and a double space adds a word. So the test checks
count_words("") == 0andcount_words("hi there") == 2. Both0and2come from the meaning of "word," not fromcount_words.Why: an expected value computed by the code under test compares the code with itself and can never fail.
-
2
Register the test with
add_test, build, and run it. It must fail (red), and the failure must be the one in the report.Example
pass: three words FAIL: empty text has no words FAIL: two spaces between words 2 failedWith CTest:
ctest --test-dir build -R words --output-on-failureshows the test as failed and prints these lines.- it fails the way the report says — the test reproduces the bug; go on.
- it passes — the test doesn't reach the bug, or the report is wrong. Fix the test before touching the code.
- it fails some other way (won't compile, crashes somewhere unrelated) — fix the test itself first.
Why: a test that has never failed has never shown it can fail. Seeing it red is the proof it tells buggy code from fixed code.
-
3
Run the failing test program the way you'd debug any wrong answer. If it crashes or misbehaves, run it in the sanitizer build. If it just gives the wrong value, run it in the debugger with a breakpoint in the function under test.
Example
Stepping through
count_words(""): the loop never runs,spacesstays0, and the function returnsspaces + 1, which is1. Counting spaces was the wrong idea.Why: the test gives you a small, fixed input that reaches the bug, which is exactly what the debugging recipe's first step asks for.
-
4
Change the code under test as little as possible. Run the new test until it passes (green), then run every test.
Example
Count a word each time a non-space character follows a space or the start of the text. The new checks pass, and so does the old
"one two three" == 3. Then plainctest --test-dir buildruns the whole suite.Why: the new test proves this bug is gone; the whole suite proves the fix didn't break something that used to work.
-
5
Leave the test in the suite as a regression test. Add any edge case the fix made you think of.
Example
Fixing
count_wordssuggested a new edge, spaces at both ends:count_words(" padded ") == 1. It joined the suite with the other three.⚡ Shortcut
If you wrote a test after the code, do the red step in reverse: break the code on purpose (change
>=to>at a boundary), confirm the test fails, then undo the change.Why: a test that stays in the suite stops this bug from coming back unnoticed.
Common mistakes
- Fixing the code first and writing the test afterwards, so you never see it fail.
- Copying the "expected" value from the program's own output.
- Writing a check that's always true, like
v.size() >= 0on an unsigned size. - Running only the new test and not the whole suite.
- Deleting the test once it passes.