Day 043 — Table-Driven Tests & Subtests¶
Month 2 · Week 3 · ⬅ Day 042 · Day 044 ➡ · Journal index
🎯 Learning Objective¶
Write idiomatic Go tests using the table-driven pattern and t.Run subtests, so each case is named, isolated, and individually runnable.
📚 Topics¶
testing.T,*_test.go, theTestXxx(t *testing.T)signature- Table-driven cases as a
[]struct - Subtests with
t.Run(name, func)·t.Errorvst.Fatal
📖 Reading / Sources¶
-
testingpackage docs - Go Blog — Using Subtests and Sub-benchmarks
- Go Wiki — TableDrivenTests
- Learning Go ch.15 (Writing Tests)
📝 Notes¶
- A test file ends in
_test.go, lives in the same package (white-box) orpackage foo_test(black-box), and exportsfunc TestXxx(t *testing.T).go test ./...discovers them. - Table-driven tests: declare a slice of anonymous structs (one row per case, with a
namefield) and loop. Adding a case is one line, not one function → [[table-driven-tests]]. - Subtests: wrap each row in
t.Run(tc.name, func(t *testing.T){...}). Benefits: named failures,go test -run TestX/case_nameto target one, and independent failure isolation. t.Error/t.Errorfmark the test failed but keep going (good for collecting all failing rows);t.Fatal/t.Fatalfcallruntime.Goexitand stop that subtest (use when later assertions would panic).- Failure messages should read got vs want:
t.Errorf("F(%q) = %q; want %q", in, got, want). No assertion library needed. t.Fatalonly stops the current goroutine — never call it from a goroutine you spawned; the test won't actually fail. → [[t-fatal-goroutine-trap]]- Subtest names get spaces replaced by
_in the CLI; keep them short and unique.
💻 Code Examples¶
func TestReverse(t *testing.T) {
cases := []struct {
name, in, want string
}{
{"empty", "", ""},
{"ascii", "Hello", "olleH"},
{"accented", "héllo", "olléh"}, // é stays one rune
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := Reverse(tc.in); got != tc.want {
t.Errorf("Reverse(%q) = %q; want %q", tc.in, got, tc.want)
}
})
}
}
Full code:
exercises/month-02/week-3/reverse/· Run:go test ./exercises/month-02/week-3/reverse
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
Reverse rune-aware reversal + table test |
✅ | exercises/month-02/week-3/reverse |
wordfreq.Count/Top with named subtests |
✅ | exercises/month-02/week-3/wordfreq |
🐛 Mistakes Made¶
- Used
t.Fatalinside at.Runsubtest expecting later rows to still run — they did, becauseFatalonly stops that subtest's goroutine. That's actually the right behavior; my mental model was off. - Compared maps with
==(compile error) → switched toreflect.DeepEqual.
❓ Open Questions¶
- When is black-box (
package foo_test) worth the import friction over white-box? (Answer forming: when you want to test only the exported API and avoid coupling to internals.)
🧠 Active Recall (answer without looking)¶
- Q: Difference between
t.Errorandt.Fatal?A
Error marks failure and continues; Fatal marks failure and stops the current test/subtest goroutine via runtime.Goexit. Never call Fatal from a spawned goroutine.
2. Q: Why give each table row a name and wrap it in t.Run? A
Named subtests pinpoint which case failed, isolate failures, and let you run one with go test -run TestX/name.
🪶 Feynman Reflection¶
A table-driven test is a spreadsheet of inputs and expected outputs that one loop walks through. t.Run turns each row into its own mini-test with a name, so the report tells you exactly which row broke instead of a single anonymous failure.
🕳️ Knowledge Gaps¶
- Setup/teardown patterns (
TestMain, per-subtest cleanup) — covered tomorrow witht.Cleanup/helpers.
✅ Summary¶
I can structure tests as named rows looped through t.Run, and I know when to keep going (Error) vs stop (Fatal).
⏭️ Next Steps / Prep for Tomorrow¶
- Day 044: parallel subtests with
t.Parallel, the loop-variable capture trap, andt.Helper/t.Cleanup.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: test(week-3): table-driven tests and subtests (day 043)