Theory

Table-driven tests — catching the lesson-7 bug

One input per test is thin. Real functions need checking against several cases — normal, edge, tricky. Writing a separate Test… for each is noise. The Go idiom is a table-driven test: a slice of {input, want} rows, and one loop that checks them all.

Recall charCount from lesson 7 — it returns how many characters are in a string. Here's a table test for it:

func TestCharCount(t *testing.T) {
    cases := []struct {
        in   string
        want int
    }{
        {"obuolys", 7},
        {"ąžuolas", 7}, // Lithuanian: 7 characters, but 9 bytes
        {"Go", 2},
        {"", 0},
    }
    for _, c := range cases {
        got := charCount(c.in)
        if got != c.want {
            t.Errorf("charCount(%q) = %d, want %d", c.in, got, c.want)
        }
    }
}

The cases := []struct{…}{…} is an anonymous struct slice — a throwaway type that exists just to list the rows. Each row is one case; the loop runs them all and reports every mismatch independently. Adding a new case is one line, not a new function.

Now the spine — this test is a trap set for a real bug. Remember lesson 7: the naive way to count characters is len(s), and it's wrong for Lithuanian, because len counts bytes and "ą" is 2 bytes. Suppose charCount still had the bug:

func charCount(s string) int {
    return len(s) // BUG: counts BYTES, not characters
}

Run go test:

--- FAIL: TestCharCount (0.00s)
    text_test.go:18: charCount("ąžuolas") = 9, want 7
FAIL
FAIL    yourapp   1.7s

There it is. The machine caught it — = 9, want 7 — the exact bug, named, before any user ever typed a Lithuanian title. No staring at output, no guessing: the failing line points straight at the case that broke.

Now fix charCount the way lesson 7 taught:

import "unicode/utf8"

func charCount(s string) int {
    return utf8.RuneCountInString(s) // counts CHARACTERS
}

Run go test again:

ok      yourapp   0.5s

And with -v you see it pass by name:

=== RUN   TestCharCount
--- PASS: TestCharCount (0.00s)
PASS

That's the whole point of testing. The bug from lesson 7 used to hide until a specific input hit it in production. Now a test remembers it forever: if anyone ever "simplifies" charCount back to len, the test goes red on the next run. A test is a bug you only have to catch once.

Next: write the full test file for your app — charCount, truncateRunes and validateTitle, all in one _test.go.