Unit Test Author
A unit test is an executable claim about behavior: given this situation, the code does this. This skill writes tests that make sharp claims — tests that fail loudly when the behavior regresses and stay green through refactors that preserve it. The enemy is the test that exercises code without asserting anything a user of that code cares about: coverage theater.
When to use this skill
- New code needs tests, before or alongside its implementation
- A bug was found — it needs a failing test before the fix and a green one after
- Existing tests break on every refactor because they assert implementation, not behavior
- A module's test gaps need mapping before a risky change lands on top of it
Instructions
- List the behaviors before writing a test. Read the unit's contract (signature, docs, callers) and enumerate the claims worth pinning: the happy path, each documented option, boundary values, error behavior, and any invariant ("never returns null", "output is sorted"). Each claim becomes one test.
- Name tests as claims: the situation and the expectation, readable in the runner's failure list without opening the file. "expired token is rejected with an auth error" — not "test token 2". A failing name should tell you what broke before you read a line of code.
- Structure every test in three visible movements — arrange, act, assert — with one action per test, and assertions that all concern that action's outcome. Multiple unrelated assertions in one test hide which claim actually failed.
- Assert on observable behavior: return values, raised errors, emitted events, state readable through the public interface. Reaching into private state or asserting call sequences on internal helpers welds the test to today's implementation; such tests fight refactoring instead of protecting it.
- Isolate with the lightest touch that keeps the test deterministic. Fake the boundaries — clock, randomness, network, filesystem — and keep everything else real. Every mock is a place the test can lie; when a collaborator is cheap and deterministic, use the real one.
- Cover the edges deliberately: empty collections, zero and negative numbers, boundary lengths, duplicate keys, unusual characters in strings, the largest plausible input. Pick edges by asking what the type system fails to forbid.
- Watch each new test fail once — by reverting the fix or negating the assertion — before trusting its green. A test that has never failed is an unverified claim about the test itself.
- Audit the suite you leave behind: every behavior from step 1 has a test, no test asserts nothing, and the whole thing runs fast enough that people run it before pushing.
Output format
Tests in the project's existing framework and directory conventions, one behavior per test, claim names throughout. Alongside the code, a short gap note: behaviors identified, behaviors covered, and anything deliberately untested with the reason (needs an integration harness, nondeterministic, out of scope).
Worked example
Claim list for a parse_duration(text) helper that returns seconds:
- parses bare seconds: "90" gives 90
- parses minute suffix: "5m" gives 300
- parses compound values: "1h30m" gives 5400
- rejects negative values with a range error
- rejects unknown units, naming the bad unit in the error message
- empty string is rejected, not treated as zero — the boundary most callers get wrong
Six claims, six tests, and the last one catches the bug report before it exists.
Guardrails
- No test without an assertion that can fail; "it runs without raising" is a smoke claim and must say so in its name.
- No sleeping and no wall-clock dependence — inject the clock, or the test inherits the flake.
- Fixtures stay minimal and local; a shared thirty-field fixture couples every test to every field.
- If code is untestable without heroic mocking, that is design feedback to surface to the user, not a mandate to build the heroic mock. Say it.