Step 5 of 6
Fixing bugs test-first
Tests aren't only for after the code is written. Many developers write them first.
Test-driven development
TDD is a short loop, repeated many times:
- Red: write a test for the next small piece of behavior. Run it and watch it fail (that proves the test can fail).
- Green: write the simplest code that makes it pass.
- Refactor: clean up the code, rerunning the tests to make sure nothing broke.
Bugs: reproduce, then fix
Even teams that don't do strict TDD follow it for bugs. When a bug is reported, first write a test that reproduces it. It fails. Then fix the code until it passes. That test, a regression test, stays in the suite forever, so the bug can't quietly come back.
A bug report says "two spaces between words count as an extra word":
fn word_count(text: &str) -> usize {
text.split(' ').count()
}
fn main() {
println!("{}", word_count("hello world"));
println!("{}", word_count("hello world"));
println!("{}", word_count(""));
}
2
3
1
Splitting on single spaces creates empty "words" between two spaces (and one empty word for an empty string). The regression tests come first:
#[test]
fn double_spaces_do_not_add_words() {
assert_eq!(word_count("hello world"), 2);
}
#[test]
fn empty_text_has_no_words() {
assert_eq!(word_count(""), 0);
}
Both fail. The fix is to split on runs of whitespace, and now everything passes:
fn word_count(text: &str) -> usize {
text.split_whitespace().count()
}
fn main() {
assert_eq!(word_count("hello world"), 2);
assert_eq!(word_count("hello world"), 2);
assert_eq!(word_count(""), 0);
assert_eq!(word_count(" tabs\tand\nnewlines "), 3);
println!("all fixed");
}
all fixed
Reading a failing test
When cargo test reports a failure, read it top to bottom: which test failed, the panic message (for assert_eq!, the left and right values), and the file and line. A test can fail because the code is wrong or because the test is wrong; check the expected value against the specification before "fixing" the code.
Common mistakes
- Fixing first, testing later (or never). Without a test that failed before the fix, you don't know the fix fixed anything.
- Deleting or loosening a failing test to make the suite green. The test is usually telling you something true.
- Fixing only the reported input. Ask what the bug's whole family looks like (two spaces, three spaces, tabs, leading spaces) and test a few.
Your task
average has a bug, and the starter's tests already show it: one of them fails. Fix average so that an empty slice gives None (instead of dividing by zero), keep both tests, and add a third test of your own (for example, that the average rounds down).
Challenge 2
initials should turn a name into its capitalized initials: ada lovelace gives AL. Words are separated by any whitespace (spaces or tabs), and an empty name has no initials. The starter's tests show two bugs: extra spaces crash it, and so does a name starting with an accented letter. Fix initials so all three tests pass (keep them); hidden tests also check tabs, an empty name and other non-ASCII letters.
Challenge 3
Practice the TDD loop from scratch. Write parse_time(s: &str) -> Option<u32>, which turns "HH:MM" into minutes after midnight ("01:30" gives 90). Hours go from 0 to 23 and minutes from 0 to 59; anything else (a missing colon, letters, "24:00", "12:60") gives None.
Write at least 3 tests first, watch them fail against todo!(), then write the function until they pass.
Previous: Testing failures: should_panic and Result Next: Organizing tests