Write tests for $ARGUMENTS — a file, function, or module. If $ARGUMENTS is empty, target the most recently changed code (git diff / git status, else the newest source files) and state what you chose before writing.
Detect conventions first
Never assume a stack. Before writing a line:
- Identify the test runner and assertion style from the manifest and lockfile (
package.jsonscripts,pyproject.toml/pytest.ini,go.mod,Cargo.toml,*.csproj, etc.). - Open 2-3 existing test files. Copy their file location and naming (
*.test.ts,*_test.go,test_*.py), import style, setup/teardown, factory/fixture helpers, and assertion vocabulary. Match them exactly. - Greenfield (no test files yet): follow the framework's canonical layout and idioms (e.g.
tests/+test_*.pyfor pytest,_test.gobeside the source withfunc TestXxx(t *testing.T)for Go,__tests__/or*.test.tsfor Jest/Vitest). Add the runner as a dev dependency and a test script if none exists, and state that you did. - Check whether the target already has a test file; if so, extend it in place rather than creating a parallel duplicate. Only add a new file when none covers this target.
- Note how the suite runs (
npm test,pytest,go test ./...) and how to run a single file, so you can iterate fast.
Understand the target
Read the target and its direct collaborators. List every observable behaviour: return values, thrown errors, state mutations, and calls to true boundaries (network, DB, filesystem, clock, randomness). This list is your test plan — write tests against it, not against the implementation's internal steps.
Write the tests
- Cover, per behaviour: the happy path, edge cases (empty, null, zero, boundary, unicode, large, duplicate), and every error/exception path.
- One behaviour per test: every assertion in a test verifies that same behaviour. Structure each as Arrange-Act-Assert with a blank line between phases.
- Name tests as a sentence describing the behaviour and condition, e.g.
returns 401 when the token is expired— nottest1or the method name. - When the stack has an idiomatic table-driven or parametrized form (Go subtests via
t.Runover a cases slice,@pytest.mark.parametrize, Jest/Vitestit.each), use it to sweep many input/output rows through one behaviour instead of copy-pasting near-identical tests. Give each row a distinct name/label. - Assert on specific values and error types/messages, not just truthiness or "no throw".
- Use real collaborators when they are fast and deterministic. Mock/stub only true boundaries and non-determinism (time, uuid, network). Do not mock the code under test.
- Make tests deterministic and isolated: no shared mutable state, no order dependence, no real sleeps, freeze the clock, seed randomness.
- Test the public contract. Do not export or reach into privates just to test them; if something is hard to test, note it as a design smell rather than weakening the test.
Run and confirm
- Run the new tests, then the full suite. Fix real failures.
- If a test fails because the code is genuinely wrong, stop and report the bug — do not bend the test to pass.
- Confirm the suite is green and report: what you tested, the behaviours covered, any gaps you deliberately left, and the exact command to run them.
Rules
- Never write tests that pass regardless of behaviour (tautologies, unconditional assertions, snapshots you did not read).
- Never delete or weaken existing tests to make the suite green.
- Never add a new test framework or dependency when the project already has one.