Writing Tests
A useful test fails when behavior is wrong, passes when it is right, and
explains the failure without a debugger. Follow repository instructions first.
Prove the behavior
- Choose the lowest layer that proves the contract: unit for local logic,
integration for real boundaries, CLI/UI/E2E only for behavior unavailable
below them.
- Exercise production code. Do not copy its logic into the test or mock the
unit under test.
- A bug fix needs a focused regression test. Where practical, revert or mutate
the fix and confirm that exact test fails.
- Assert observable results, not private calls or incidental structure.
- Print expected and actual values, variants, exit status, stderr, and case
labels needed to diagnose a rare CI failure.
- Cover material success, failure, boundary, default, and state transitions.
Do not add combinations without a concrete regression risk.
Control every input
- Inject clocks; fix timezone and locale when formatting matters.
- Seed randomness and print the replay seed on failure.
- Use semantic unordered equality when order is not a contract. Do not hide a
required order by sorting expected output.
- Give each test an isolated environment, home, working directory, config root,
database state, cache, and temporary directory.
- Bind servers to port
0 and retain the listener. Never find, close, and
rebind a "free" port.
- Avoid process-global mutation from parallel tests. Use helpers that restore
state automatically.
Synchronize; do not sleep
Replace sleeps and grace periods with a happens-before edge: channel receive,
barrier, latch, event, condition, queue drain, file close, EOF, process exit, or
application readiness.
Use fake time only when time is the contract. Know which clock it controls.
Use a timeout only around an observable wait as an outer deadlock detector;
expiry must fail with diagnostics. Never use a sleep, retry, timeout increase,
or looser assertion as a race fix.
For processes, establish the required order: start output readers, close stdin
when EOF is required, drain stdout and stderr, observe exit, then clean up.
Preserve output even when finalization fails.
Own resources and cleanup
Register cleanup immediately after acquisition. Teardown in dependency order:
stop producers, cancel work, drain or join consumers, flush, close resources,
then delete storage. Surface cleanup failures.
Parallel tests need separate resources. A shared temp path, port, database row,
clock, fake timer, module cache, or mutable global is a race even if it usually
passes.
Use a local implementation of the real protocol at boundaries: an in-process
HTTP server, transaction-isolated database, or controlled fake implementing the
real interface. Do not put live third-party availability, credentials, mutable
data, or rate limits in required tests.
Snapshots, properties, and fuzzing
Normalize only fields proven volatile, such as generated IDs or temp roots.
Broad normalization can erase the bug. Golden updates must be explicit and
reviewed.
Use property, fuzz, differential, or metamorphic tests when examples cannot
cover the input space or no simple expected value exists. Retain the minimized
input or seed so every discovery becomes a deterministic regression.
E2E, browser, CLI, and TUI
Wait for the exact readiness needed by the next action: health response, UI
state, log event, or prompt. Do not use arbitrary sleep or generic network idle.
Prefer user-visible and accessibility-based locators plus retrying assertions.
Control browser, viewport, fonts, animation, and dynamic content for visual
tests. Assert CLI/TUI exit status and relevant stdout/stderr after complete
drain. For AI-backed behavior, assert an objective observable contract, not one
exact natural-language answer unless the text itself is the contract.
Language mechanics
Go
- Use
t.TempDir, t.Setenv, t.Cleanup, httptest.Server, the race detector,
and shuffled runs where relevant. Record shuffle seeds.
- Process-global environment and working-directory changes are incompatible
with parallel tests.
FailNow and testify require must run on the test goroutine. From handlers
or spawned goroutines, report errors to the test goroutine and synchronize.
- Use channels and
WaitGroup; use testing/synctest only when supported by
the repository's Go version.
- Compare maps semantically or sort a copied key set for golden output.
Rust
- Keep test-only helpers behind
#[cfg(test)].
- Use channels and barriers for ordering; use
#[tokio::test(start_paused = true)] for timer behavior. Prefer
time::sleep(d).await over tokio::time::advance(d), which does not wait
for tasks to be polled and can produce false passes.
- When already present, Loom or Shuttle can explore schedules and Proptest can
shrink and persist failures.
- Ensure all process pipe owners close so EOF and exit are observable.
TypeScript and JavaScript
- Always await promise assertions and async timer advancement.
- Restore spies, modules, and fake timers after each test; clearing calls does
not restore implementations.
- Do not combine process-global fake timers or mutable module state with
concurrent tests.
- Use Playwright locators and web assertions for readiness.
- For subprocess output assertions, await the
'close' event, not 'exit';
'exit' fires before stdio streams finish flushing.
Python
- Use
tmp_path and monkeypatch for automatically restored state.
- Cancel and await asyncio tasks before ending a test:
task.cancel(); with contextlib.suppress(asyncio.CancelledError): await task.
- Use
subprocess.communicate() for bidirectional process I/O.
- When order or property tools are already present, retain their seed or
minimized example.
Flake triage
A passing rerun on the same commit proves nondeterminism, not correctness.
- Read the original failure text, stack, stderr, seed, logs, and artifacts.
- Separate infrastructure failures such as OOM, disk, runner, or container
loss from product assertions.
- Compare the failure window with the lifetime of the failing code. Use history
to name the introducing or fixing commit; absence of recent failures is not
evidence of a fix.
- Inspect every CI attempt because rerun-to-green summaries hide failures.
- Reproduce on parents when needed to distinguish a landed regression or
semantic merge conflict from a race.
- Identify the mechanism: ordering, async completion, data race, resource leak,
state pollution, clock, randomness, platform, external dependency, or
infrastructure.
- Fix the owning layer and write a deterministic regression test controlling
that mechanism.
Prioritize flaky required checks because they block merges. Retries or
quarantine may temporarily unblock work, but they must retain failed attempts,
an owner, and an issue. Do not weaken assertions or delete safety coverage to
make CI green.
Related skills
code-review: coverage and determinism review. When invoked from
code-review, do not invoke it again.
runtime-process-debugging: process, pipe, lifecycle, shutdown, and ordering
mechanisms. When invoked from it, do not invoke it again.
go-conventions and rust-specialist: language-specific production changes.
change-impact-auditor: tests for configuration, protocol, default, or shared
model changes.
Do not recurse. Keep the fix to one concern and run the smallest decisive
validation.
1---2name: writing-tests3description: Write deterministic tests that fail for real product defects. Use when adding tests, fixing flakes, or reviewing test coverage and reliability.4---56# Writing Tests78A useful test fails when behavior is wrong, passes when it is right, and9explains the failure without a debugger. Follow repository instructions first.1011## Prove the behavior1213- Choose the lowest layer that proves the contract: unit for local logic,14 integration for real boundaries, CLI/UI/E2E only for behavior unavailable15 below them.16- Exercise production code. Do not copy its logic into the test or mock the17 unit under test.18- A bug fix needs a focused regression test. Where practical, revert or mutate19 the fix and confirm that exact test fails.20- Assert observable results, not private calls or incidental structure.21- Print expected and actual values, variants, exit status, stderr, and case22 labels needed to diagnose a rare CI failure.23- Cover material success, failure, boundary, default, and state transitions.24 Do not add combinations without a concrete regression risk.2526## Control every input2728- Inject clocks; fix timezone and locale when formatting matters.29- Seed randomness and print the replay seed on failure.30- Use semantic unordered equality when order is not a contract. Do not hide a31 required order by sorting expected output.32- Give each test an isolated environment, home, working directory, config root,33 database state, cache, and temporary directory.34- Bind servers to port `0` and retain the listener. Never find, close, and35 rebind a "free" port.36- Avoid process-global mutation from parallel tests. Use helpers that restore37 state automatically.3839## Synchronize; do not sleep4041Replace sleeps and grace periods with a happens-before edge: channel receive,42barrier, latch, event, condition, queue drain, file close, EOF, process exit, or43application readiness.4445Use fake time only when time is the contract. Know which clock it controls.46Use a timeout only around an observable wait as an outer deadlock detector;47expiry must fail with diagnostics. Never use a sleep, retry, timeout increase,48or looser assertion as a race fix.4950For processes, establish the required order: start output readers, close stdin51when EOF is required, drain stdout and stderr, observe exit, then clean up.52Preserve output even when finalization fails.5354## Own resources and cleanup5556Register cleanup immediately after acquisition. Teardown in dependency order:57stop producers, cancel work, drain or join consumers, flush, close resources,58then delete storage. Surface cleanup failures.5960Parallel tests need separate resources. A shared temp path, port, database row,61clock, fake timer, module cache, or mutable global is a race even if it usually62passes.6364Use a local implementation of the real protocol at boundaries: an in-process65HTTP server, transaction-isolated database, or controlled fake implementing the66real interface. Do not put live third-party availability, credentials, mutable67data, or rate limits in required tests.6869## Snapshots, properties, and fuzzing7071Normalize only fields proven volatile, such as generated IDs or temp roots.72Broad normalization can erase the bug. Golden updates must be explicit and73reviewed.7475Use property, fuzz, differential, or metamorphic tests when examples cannot76cover the input space or no simple expected value exists. Retain the minimized77input or seed so every discovery becomes a deterministic regression.7879## E2E, browser, CLI, and TUI8081Wait for the exact readiness needed by the next action: health response, UI82state, log event, or prompt. Do not use arbitrary sleep or generic network idle.83Prefer user-visible and accessibility-based locators plus retrying assertions.8485Control browser, viewport, fonts, animation, and dynamic content for visual86tests. Assert CLI/TUI exit status and relevant stdout/stderr after complete87drain. For AI-backed behavior, assert an objective observable contract, not one88exact natural-language answer unless the text itself is the contract.8990## Language mechanics9192### Go9394- Use `t.TempDir`, `t.Setenv`, `t.Cleanup`, `httptest.Server`, the race detector,95 and shuffled runs where relevant. Record shuffle seeds.96- Process-global environment and working-directory changes are incompatible97 with parallel tests.98- `FailNow` and testify `require` must run on the test goroutine. From handlers99 or spawned goroutines, report errors to the test goroutine and synchronize.100- Use channels and `WaitGroup`; use `testing/synctest` only when supported by101 the repository's Go version.102- Compare maps semantically or sort a copied key set for golden output.103104### Rust105106- Keep test-only helpers behind `#[cfg(test)]`.107- Use channels and barriers for ordering; use108 `#[tokio::test(start_paused = true)]` for timer behavior. Prefer109 `time::sleep(d).await` over `tokio::time::advance(d)`, which does not wait110 for tasks to be polled and can produce false passes.111- When already present, Loom or Shuttle can explore schedules and Proptest can112 shrink and persist failures.113- Ensure all process pipe owners close so EOF and exit are observable.114115### TypeScript and JavaScript116117- Always await promise assertions and async timer advancement.118- Restore spies, modules, and fake timers after each test; clearing calls does119 not restore implementations.120- Do not combine process-global fake timers or mutable module state with121 concurrent tests.122- Use Playwright locators and web assertions for readiness.123- For subprocess output assertions, await the `'close'` event, not `'exit'`;124 `'exit'` fires before stdio streams finish flushing.125126### Python127128- Use `tmp_path` and `monkeypatch` for automatically restored state.129- Cancel and await asyncio tasks before ending a test:130 `task.cancel(); with contextlib.suppress(asyncio.CancelledError): await task`.131- Use `subprocess.communicate()` for bidirectional process I/O.132- When order or property tools are already present, retain their seed or133 minimized example.134135## Flake triage136137A passing rerun on the same commit proves nondeterminism, not correctness.1381391. Read the original failure text, stack, stderr, seed, logs, and artifacts.1402. Separate infrastructure failures such as OOM, disk, runner, or container141 loss from product assertions.1423. Compare the failure window with the lifetime of the failing code. Use history143 to name the introducing or fixing commit; absence of recent failures is not144 evidence of a fix.1454. Inspect every CI attempt because rerun-to-green summaries hide failures.1465. Reproduce on parents when needed to distinguish a landed regression or147 semantic merge conflict from a race.1486. Identify the mechanism: ordering, async completion, data race, resource leak,149 state pollution, clock, randomness, platform, external dependency, or150 infrastructure.1517. Fix the owning layer and write a deterministic regression test controlling152 that mechanism.153154Prioritize flaky required checks because they block merges. Retries or155quarantine may temporarily unblock work, but they must retain failed attempts,156an owner, and an issue. Do not weaken assertions or delete safety coverage to157make CI green.158159## Related skills160161- `code-review`: coverage and determinism review. When invoked from162 `code-review`, do not invoke it again.163- `runtime-process-debugging`: process, pipe, lifecycle, shutdown, and ordering164 mechanisms. When invoked from it, do not invoke it again.165- `go-conventions` and `rust-specialist`: language-specific production changes.166- `change-impact-auditor`: tests for configuration, protocol, default, or shared167 model changes.168169Do not recurse. Keep the fix to one concern and run the smallest decisive170validation.