Testing Code That Accepts context.Context
Functions that accept context.Context need tests for success, deadline exceeded, and manual cancellation - not just the happy path.
Go's context package and testing helpers make those paths deterministic without sleeping in CI.
Summary
Use context.WithTimeout or WithCancel in tests to drive ctx.Err() outcomes.
Prefer channel-based fakes over time.Sleep when testing cancel propagation.
Assert with errors.Is for context.Canceled and context.DeadlineExceeded.
Recipe
Quick-reference recipe card - copy-paste ready.
func TestWorkCanceled(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
err := Work(ctx)
if !errors.Is(err, context.Canceled) {
t.Fatalf("got %v, want Canceled", err)
}
}When to reach for this:
- Table-test
ctxoutcomes alongside normal inputs. - Use short timeouts (milliseconds) instead of multi-second sleeps.
- Test HTTP handlers with
httptestand request contexts.
Working Example
package work_test
import (
"context"
"errors"
"testing"
"time"
"example.com/ctxdemo/work"
)
func TestFetchDeadline(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
defer cancel()
err := work.Fetch(ctx, slowStub{})
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("got %v", err)
}
}
func TestFetchCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(5 * time.Millisecond)
cancel()
}()
err := work.Fetch(ctx, slowStub{})
if !errors.Is(err, context.Canceled) {
t.Fatalf("got %v", err)
}
}
type slowStub struct{}
func (slowStub) Run(ctx context.Context) error {
select {
case <-time.After(200 * time.Millisecond):
return nil
case <-ctx.Done():
return ctx.Err()
}
}What this demonstrates:
DeadlineExceededfrom a shortWithTimeoutwithout long sleeps in the assertion path.- Manual
cancel()from another goroutine simulates client disconnect. - Stub dependency respects
ctxthe same way production I/O would. errors.Iskeeps assertions stable if errors wrap with%w.
Deep Dive
How It Works
- Tests construct contexts independently of production servers - no special test-only API required.
t.Context()(Go 1.24+) provides a context canceled when the test ends - useful for integration tests.httptest.NewRequestattaches a background context; replace withreq.WithContext(ctx)for cancel cases.- Fake clocks are rarely needed; sub-50ms timeouts keep tests fast on CI.
Test Matrix
| Scenario | Setup | Expected |
|---|---|---|
| Success | context.Background() | nil error |
| Timeout | WithTimeout(..., 1ms) | DeadlineExceeded |
| Manual cancel | cancel() before call | Canceled |
| Parent canceled | cancel parent of child | child Err() set |
| Cause attached | WithCancelCause | context.Cause(ctx) |
HTTP Handler Test
req := httptest.NewRequest(http.MethodGet, "/", nil)
ctx, cancel := context.WithCancel(req.Context())
cancel()
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
handler(rec, req)
// expect no response body on cancelGo Notes
func wait(ctx context.Context, d time.Duration) error {
timer := time.NewTimer(d)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
}
}Always stop timers in tests and production to avoid leaks.
Gotchas
- Sleeping to hit deadlines - Flaky on loaded CI runners. Fix: use immediate
cancel()or 1-20ms timeouts. - Comparing errors with == - Wrapped errors fail
==. Fix:errors.Isanderrors.As. - Not calling cancel in tests - Timer leak warnings in
-raceruns. Fix:defer cancel()always. - Using Background in handler tests - Misses disconnect behavior. Fix: inject cancelable ctx via
WithContext. - Testing only DeadlineExceeded - Cancel paths differ in logging and metrics. Fix: cover both terminal errors.
- Shared ctx across parallel subtests - Race on
cancel(). Fix: per-subtest context witht.Parallel()care.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
t.Context() | Auto cleanup on test end | Fine-grained cancel timing control |
| Interface fake with blocking channel | Deterministic cancel | Simple pure functions |
| Integration test with real DB | Driver cancel verification | Unit-level logic tests |
testing/quick | Randomized deadline fuzz | Readable table tests |
time.Sleep in tests | Never preferred | - |
FAQs
Should I test context in every function?
Test functions that branch on ctx.Done() or pass ctx to dependencies.
Pure helpers that only forward ctx can rely on integration tests.
How do I test context.WithValue?
Attach values in test setup with the same key helpers production uses.
Assert accessors return expected metadata.
Can I use testify assert?
Yes - assert.ErrorIs(t, err, context.Canceled) reads well in table tests.
How do I test gRPC interceptors?
Build a context with timeout and invoke the interceptor with httptest or grpc's test buffers.
Verify codes.DeadlineExceeded mapping.
What about context.Cause?
Cancel with WithCancelCause and assert errors.Is(context.Cause(ctx), wantCause).
Should benchmarks use context?
Use context.Background() unless benchmarking cancel overhead specifically.
Report -benchmem separately from cancel tests.
How do I avoid timer leaks?
Always defer cancel() for WithTimeout and stop timers in select loops.
Can subtests share a table ctx?
Build a fresh ctx per row inside t.Run to avoid cross-test pollution.
How do I test WithoutCancel?
Cancel parent, derive context.WithoutCancel(parent), and assert child still runs briefly.
Verify parent values are visible.
Does -race help?
Yes - run cancel tests with -race to catch goroutines that ignore Done().
Related
- context Basics - API primitives under test
- Cancellation Propagation in HTTP Handlers - httptest wiring
- Deadlines & Timeouts Across Service Boundaries - budget logic to assert
- context in database/sql & gRPC - integration cancel behavior
- Context Misuse Anti-Patterns - bugs tests should catch
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).