In a Go test, when is the context from t.Context() cancelled relative to t.Cleanup functions?
answer
- cancelled first, then torn down
- the code under test stops before teardown
- a cleanup sees a done context
- bring your own background context to cleanups
- the parent's waits for its subtests
basics
~10 sIt is cancelled just before the test's registered cleanup functions are called. Every cleanup therefore sees an already-cancelled context, so teardown work that needs a live context must build its own from context.Background().
solid answer
~50 s`t.Context()` hands the test a `context.Context` whose lifetime is the test's, and it is cancelled *just before* the functions registered with `t.Cleanup` start running. The ordering is deliberate: pass `t.Context()` into the code under test, and when the test ends that code is told to stop first, so cleanups run against a system that is already winding down rather than racing it. The consequence to remember is that a cleanup must never use `t.Context()` for real work — the deadline is already blown, and any call that honours cancellation will fail instantly with `context.Canceled`. A cleanup that has to make one last call (delete a remote fixture, flush a file, drop a database) should derive its own context from `context.Background()` with its own timeout. For a parent with parallel subtests the cancellation lands after those subtests have completed, since that is when the parent's cleanups run.
code
go · 14 linesfunc TestUnpack(t *testing.T) {
dir := t.TempDir()
if err := unpack(t.Context(), "fixture.tar", dir); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
// t.Context() is already canceled here.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := purgeRemote(ctx, dir); err != nil {
t.Errorf("purge: %v", err)
}
})
}go deeper
Recall that t.Context() gives a test a context tied to that test's lifetime, so you no longer hand-roll one from context.Background() just to call the code under test.
Explain the exact ordering: cancellation lands just before cleanup functions run, which is why a cleanup must build its own context for any work that honours cancellation.
Show the diagnosis: a teardown that always logs context canceled and leaves fixtures behind, in a test that otherwise passes. Also bound the cleanup's own context so a hang does not eat the package timeout.
Decide the convention for the codebase: what a test's cancellation is allowed to abandon versus what must still be cleaned up afterwards, and make that explicit in shared test helpers rather than per test.
## What t.Context gives you Most Go code that does anything blocking takes a `context.Context` as its first parameter. Tests therefore need one to pass in, and for years every test wrote `ctx, cancel := context.WithCancel(context.Background())` plus a `defer cancel()`, or just passed `context.Background()` and never cancelled anything. `t.Context()` (added in Go 1.24, alongside the same method on `*testing.B` and `*testing.F`) supplies a context scoped to the test, so the code under test is actually told to stop when the test ends. ## The exact timing The documented contract is that the returned context is **cancelled just before the functions registered with `t.Cleanup` are called**. Laid out for one test: 1. The test body runs, having passed `t.Context()` into the code under test. 2. The test finishes (returns, fails, or is stopped by `Fatal`). 3. For a parent test, all subtests complete. 4. **`t.Context()`'s context is cancelled.** 5. Cleanup functions run, LIFO. That ordering exists so cleanups have something stable to clean up. Background work started by the code under test — a goroutine polling, a request in flight, a watcher loop — sees `ctx.Done()` close *before* teardown begins, and a cleanup can then wait for those things to shut down before removing the resources they were using. Tearing the directory or the server out from under still-running work would produce exactly the confusing late errors that this ordering avoids. ## The consequence you must design around Because cancellation happens *before* step 5, **every cleanup function sees an already-cancelled context**. Any code inside a cleanup that honours cancellation will fail immediately: ```go t.Cleanup(func() { // ctx here is already canceled: this call fails with context.Canceled. if err := purgeRemote(t.Context(), id); err != nil { t.Errorf("purge: %v", err) } }) ``` The symptom is a teardown that always reports `context canceled` and a fixture that is never actually removed, in a test that otherwise passes. The fix is to give the cleanup its own context: ```go t.Cleanup(func() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := purgeRemote(ctx, id); err != nil { t.Errorf("purge: %v", err) } }) ``` A bounded timeout matters here: a cleanup that hangs holds the whole test binary until the package timeout fires and produces a much less readable failure. ## Scoping Each test and each subtest gets its own context. A subtest's context is cancelled when *that subtest* completes, so a parallel subtest's context closes well before the parent's. The parent's context is cancelled only once every subtest has completed, because that is when the parent's cleanups run. If you create fixtures in the parent and use them from parallel subtests, pass the parent's `t.Context()` down when you want the whole group to be cancelled together, and each subtest's own `t.Context()` when work should stop with that case alone. ## What it is not `t.Context()` is not a timeout. It does not carry a deadline of its own; it is cancelled by the lifecycle, not by the clock, and the test binary's overall `-timeout` is a separate mechanism that panics the whole run rather than cancelling this context politely. If a specific operation in a test needs a deadline, wrap it: `context.WithTimeout(t.Context(), d)`. It also is not a place to carry test fixtures. It is a cancellation signal that happens to be a `context.Context`; values belong in explicit parameters or in the test's own variables. ## Before Go 1.24 Older tests do this by hand, and the hand-written version is worth recognising in a review: `ctx, cancel := context.WithCancel(context.Background())` with `t.Cleanup(cancel)` gets close, but note that a `t.Cleanup(cancel)` registered early runs *last* under LIFO, so the cancellation lands after the other cleanups rather than before them — the opposite of the built-in ordering. That subtle difference is a good reason to use `t.Context()` where the toolchain version allows it.
- Why cancel before the cleanups rather than after them?So teardown is not racing live work. Background goroutines and in-flight calls started by the code under test see Done close first, which lets a cleanup wait for them to stop before it removes the directory, connection, or server they were using.
- A cleanup has to make one last network call. Which context should it use?One it creates itself, typically context.WithTimeout(context.Background(), d) with a defer of the cancel function. Reusing the test's context guarantees an immediate context.Canceled, and using an unbounded Background context risks a cleanup that hangs until the package timeout fires.
- Does t.Context() carry a deadline derived from the -timeout flag?No. It is a lifecycle cancellation, not a clock. The binary-wide -timeout is a separate mechanism that panics the run. Wrap the context yourself with context.WithTimeout when a particular operation needs a deadline.
saying these in an interview costs you the question
- Uses t.Context() inside a cleanup and blames flakiness
- Thinks the context stays live until the test binary exits
- Believes cancellation happens after cleanup functions run
- Treats t.Context() as a per-test timeout
- Calls context.Background() in a cleanup with no timeout at all