Writing Tests
SkillDev toolsWrite deterministic tests that fail for real product defects. Use when adding tests, fixing flakes, or reviewing test coverage and reliability.
Available today. Use it from your connected AI after setup.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the Writing Tests skill
What this skill tells your AI
The instructions your AI receives, as published by caarlos0/dotfiles in skills/writing-tests/SKILL.md and read by ahel’s review.
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
0and 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.
FailNowand testifyrequiremust run on the test goroutine. From handlers or spawned goroutines, report errors to the test goroutine and synchronize.- Use channels and
WaitGroup; usetesting/synctestonly 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. Prefertime::sleep(d).awaitovertokio::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_pathandmonkeypatchfor 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 fromcode-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-conventionsandrust-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.
Signals
- GitHub stars
- 220
- Forks
- 11
- Last commit
- Sep 2026
Advanced
- Item type
- skill
- Key
writing-tests-caarlos0- Source
- github.com/caarlos0/dotfiles