Day 044 — t.Parallel & Test Helpers¶
Month 2 · Week 3 · ⬅ Day 043 · Day 045 ➡ · Journal index
🎯 Learning Objective¶
Run subtests concurrently with t.Parallel, dodge the loop-variable capture trap, and factor shared assertions/setup into t.Helper and t.Cleanup.
📚 Topics¶
t.Parallel()semantics and the two-phase scheduler- The classic loop-variable capture bug (pre-Go 1.22)
t.Helper()for clean failure lines ·t.Cleanup()for teardown ·t.TempDir()
📖 Reading / Sources¶
-
testing— Parallel -
testing— Helper / Cleanup / TempDir - Go 1.22 release notes — loop variable scoping
📝 Notes¶
t.Parallel()pauses the calling subtest until its parent returns, then resumes it alongside all other parallel siblings. So parallel subtests run after the serial part of the parent finishes. → [[t-parallel]]- The capture trap: before Go 1.22, the loop variable
tcwas shared across iterations, so a parallel closure read whichever value the loop ended on. Fix:tc := tcshadow inside the loop. Go 1.22+ makes each iteration its own variable, but thetc := tcshadow is still a harmless, portable habit. t.Helper()marks a function as a helper so failures are reported at the caller's line, not inside the helper. Put it as the first statement of the helper.t.Cleanup(fn)registers teardown that runs when the test (or subtest) finishes, in LIFO order — cleaner thandeferbecause it survives across helper boundaries and runs even after at.Fatal.t.TempDir()returns a unique temp dir auto-removed via cleanup — perfect for filesystem tests, and each parallel test gets its own.- Parallelism multiplies flakiness if tests share state. Keep tests independent; never rely on ordering between parallel subtests.
- Tune worker count with
go test -parallel N(defaults toGOMAXPROCS).
💻 Code Examples¶
func TestThings(t *testing.T) {
cases := []struct{ name, in, want string }{
{"a", "x", "X"}, {"b", "y", "Y"},
}
for _, tc := range cases {
tc := tc // shadow: safe on every Go version
t.Run(tc.name, func(t *testing.T) {
t.Parallel() // resumes after the parent's serial part returns
assertUpper(t, tc.in, tc.want)
})
}
}
func assertUpper(t *testing.T, in, want string) {
t.Helper() // failures point at the t.Run line, not here
if got := strings.ToUpper(in); got != want {
t.Errorf("ToUpper(%q) = %q; want %q", in, got, want)
}
}
The
wordfreqexercise uses at.Helper()assertion:exercises/month-02/week-3/wordfreq/
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
Add t.Helper assertion to wordfreq tests |
✅ | exercises/month-02/week-3/wordfreq |
Make reverse subtests t.Parallel-safe |
✅ | exercises/month-02/week-3/reverse |
🐛 Mistakes Made¶
- Called
t.Parallel()but forgottc := tc; on an older toolchain every subtest tested the last row. The shadow fixed it. - Put
t.Helper()after the assertion — it must be the first line to take effect for that call.
❓ Open Questions¶
- Does
t.Cleanupregistered in a parent run before or after child subtests' cleanups? (LIFO overall; child cleanups run when the child finishes, before the parent's.)
🧠 Active Recall (answer without looking)¶
- Q: Why shadow
tc := tcbefore a parallel subtest closure?A
Pre-Go 1.22 the loop variable is shared, so the deferred/parallel closure sees the final value. Shadowing gives each iteration its own copy. Still a safe habit on 1.22+.
2. Q: What does t.Helper() change about failure output? A
It reports the failure at the caller's line instead of inside the helper, so you see which test row failed. It must be the first statement.
🪶 Feynman Reflection¶
t.Parallel() is like raising your hand and saying "wait for everyone, then go together" — the test pauses until siblings are ready, then they all run at once. t.Helper() tells the failure reporter "blame whoever called me," and t.Cleanup() is a tidy defer that survives helper boundaries and fatals.
🕳️ Knowledge Gaps¶
- Detecting data races between parallel tests — revisit with
go test -racein the tooling notes.
✅ Summary¶
I can parallelize subtests safely, attribute failures to the right line with helpers, and tear down resources reliably with t.Cleanup/t.TempDir.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 045: testing HTTP handlers with
net/http/httptest.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: test(week-3): t.Parallel, helpers and cleanup (day 044)