RustArena

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:

  1. Red: write a test for the next small piece of behavior. Run it and watch it fail (that proves the test can fail).
  2. Green: write the simplest code that makes it pass.
  3. 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

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