Subchapter 68.1
references/fix-patterns.mdMarkdown8 KBView on GitHub
These patterns are always invalid, regardless of category. A fix that only reduces flake probability instead of eliminating the root cause is not a fix.
time.Sleep, or widening timeouts/retries when the root cause is not the timeout itself (e.g. to paper over a race condition) — these mask the problem and slow the test suite. Exception: increasing a timeout IS valid when the category confirms the deadline is genuinely too tight for the operation under test.timeoutat_least(1), contains, or range checks when the exact value is deterministic and should be asserted preciselyFor each flaky_category value, apply the corresponding fix pattern. Use stack trace evidence from STEP 2 to confirm the pattern before proposing a change.
Signal: Test exceeds time limit; stack trace shows timeout or deadline exceeded.
Confirm first: Verify the deadline itself is the root cause — not an underlying race, stuck goroutine, or async ordering issue that happens to surface as a timeout. If concurrency or asynchronous_wait signals are present, fix the synchronization instead.
Preferred fix: Replace real waits with mocked/injectable time so the test runs deterministically regardless of environment speed.
type Clock interface { Now() time.Time }
// inject a fake clock in tests instead of relying on wall timeAcceptable fix: Increase the timeout when the deadline is provably too tight for the operation (e.g. a real I/O call that consistently needs more time than allowed). Increase proportionally — 2–5× the current value — not arbitrarily.
// Before — too tight for the operation
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
// After — proportional increase with justification
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)Invalid: Bumping a timeout to make a race condition or async ordering issue fail less often — this masks the real problem.
For polling loops, add a maximum retry count with exponential backoff rather than a fixed sleep.
Signal: Data race, deadlock, or non-deterministic ordering. Stack trace may show goroutine contention.
Fix: Add synchronization. Common patterns:
// Add mutex around shared state
var mu sync.Mutex
mu.Lock()
sharedState = value
mu.Unlock()
// Use sync.WaitGroup for goroutine coordination
var wg sync.WaitGroup
wg.Add(1)
go func() { defer wg.Done(); doWork() }()
wg.Wait()Signal: Assertions compare timestamps or durations that vary by wall-clock time.
Fix: Inject a clock interface; use a fake clock in tests.
// Before — uses time.Now() directly
func IsExpired(t time.Time) bool { return time.Now().After(t) }
// After — injectable clock
type Clock interface { Now() time.Time }
func IsExpired(t time.Time, clk Clock) bool { return clk.Now().After(t) }In tests, pass a fakeClock that returns a deterministic value.
Signal: Connection refused, DNS failure, or timeout on external calls.
Fix: Mock the HTTP client or network dependency in tests.
// Inject an http.Client so tests can swap it
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"ok":true}`))
}))
defer server.Close()
client := &Client{httpClient: server.Client()}Signal: Test output varies between runs with no environmental change.
Fix: Seed the random number generator with a fixed value in tests.
// Before
r := rand.Int()
// After — deterministic in tests
rng := rand.New(rand.NewSource(42))
r := rng.Int()Signal: Test passes in isolation but fails when run with other tests; order of test execution matters.
Fix: Add proper setup and teardown to isolate test state.
func TestMyFeature(t *testing.T) {
// Reset shared state before test
db.Reset()
cache.Clear()
defer func() {
db.Reset()
}()
// ... test body
}Signal: Test fails only in CI or only on certain machines; depends on env vars or installed tools.
Fix: Inject configuration rather than reading env vars directly in the function under test.
// Before — reads env var in tested function
func NewClient() *Client { return &Client{endpoint: os.Getenv("API_URL")} }
// After — injectable config
type Config struct { APIEndpoint string }
func NewClient(cfg Config) *Client { return &Client{endpoint: cfg.APIEndpoint} }Signal: File not found, permission denied, or leftover files from previous runs.
Fix: Use t.TempDir() for test files; it is cleaned up automatically after the test.
func TestFileProcessing(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "test.txt")
os.WriteFile(path, []byte("content"), 0644)
// ... test using path
}Signal: Resource exhaustion (open files, goroutines, DB connections) accumulates across tests.
Fix: Use defer to release resources; use t.Cleanup for test-scoped cleanup.
conn, err := db.Connect()
require.NoError(t, err)
t.Cleanup(func() { conn.Close() })Signal: Assertion checks for an exact value that legitimately varies (latency, size, timestamp).
Fix: Widen the assertion to a range or relative comparison.
// Before — exact match on timing
assert.Equal(t, 100*time.Millisecond, elapsed)
// After — allow variance
assert.InDelta(t, 100.0, elapsed.Milliseconds(), 50.0) // ±50msSignal: Assertion fails because slice or map iteration order varies between runs.
Fix: Sort before comparing, or use a set-based assertion.
// Before
assert.Equal(t, []string{"a", "b", "c"}, result)
// After — sort both sides, or use ElementsMatch
sort.Strings(result)
assert.Equal(t, []string{"a", "b", "c"}, result)
// Or with testify
assert.ElementsMatch(t, []string{"a", "b", "c"}, result)Signal: Floating-point equality assertion fails due to precision differences.
Fix: Use tolerance-based comparison.
// Before
assert.Equal(t, 0.3, 0.1+0.2)
// After
assert.InDelta(t, 0.3, 0.1+0.2, 1e-9)Signal: Test races against an async operation; result is checked before it completes.
Fix: Use channels, sync.WaitGroup, or a polling helper with a deadline instead of fixed sleeps.
// Before — fixed sleep, races against async completion
time.Sleep(100 * time.Millisecond)
assert.Equal(t, "done", getStatus())
// After — poll with a deadline
require.Eventually(t, func() bool {
return getStatus() == "done"
}, 5*time.Second, 50*time.Millisecond)Category could not be determined automatically. Do not attempt a code fix. Follow STEP 4/5 in the main skill: self-introduced flakes escalate to the author; pre-existing or undetermined flakes follow the impact thresholds and infrastructure guard. Include the full triage brief for manual investigation.