Skip to content

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

📝 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 tc was shared across iterations, so a parallel closure read whichever value the loop ended on. Fix: tc := tc shadow inside the loop. Go 1.22+ makes each iteration its own variable, but the tc := tc shadow 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 than defer because it survives across helper boundaries and runs even after a t.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 to GOMAXPROCS).

💻 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 wordfreq exercise uses a t.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 forgot tc := 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.Cleanup registered 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)

  1. Q: Why shadow tc := tc before 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 -race in 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)