Test design
SkillMediaWrite unit and integration tests that fail for the right reason, behaviour-level assertions, boundary values, the smallest honest double, deterministic setup and teardown. Use when adding tests for new code, pinning a bug before fixing it, or repairing tests that stay green while the code is broken. Not for deciding what to test or at which level, not for browser journeys, and not a way to call a change verified.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Test design skill
What this skill tells your AI
The instructions your AI receives, as published by nahid-sparktales/agent-dispatcher in skills/quality/test-design/SKILL.md and read by ahel’s review.
A test earns its keep only by failing when the behaviour it names is broken. Most bad tests are not wrong; they are inert — they assert on what the code does rather than what it must do, so they go red on every refactor and green through every real defect.
Keep the verbs apart: created is the test file written; executed is a run that happened; tested is a check that will run again without you; verified is a conclusion about the system, and a passing unit test does not supply it.
When this fires
Adding coverage for new or changed code, pinning a reported bug before fixing it, or fixing a suite
that passes while the behaviour is wrong. It does not fire for choosing which risks to cover at
which level (test-strategy), nor for end-to-end journeys (e2e-testing).
Procedure
- Name the behaviour in one sentence in the caller's terms — "a refund over the original amount is rejected", not "calls validateAmount". If the sentence needs an internal name to make sense, you are about to test the implementation.
- Make it fail first, for the reason you expect. New test: run it before the code exists or against the unfixed bug and read the failure message. Existing code: break the line it protects, confirm red, restore. A test never observed red is an assumption, not a check.
- Arrange the minimum, and inline what the test is about. Build entities through a factory or helper that fills irrelevant fields with defaults, and set the one or two values this test turns on explicitly in the test body. A reader should see the input that matters without opening a fixture file.
- Assert on the observable outcome — the returned value, the persisted row, the message published, the error raised. Asserting that a collaborator was called with certain arguments tests your wiring, and it will keep passing after the collaborator starts doing the wrong thing.
- One behaviour per test. A test with several unrelated assertions reports only the first failure and hides the rest. Several inputs exercising the same behaviour belong in one parameterised case, not several copies.
- Work the boundaries deliberately. For each input: zero, one, many; empty and absent (they are different); the value at the limit and the value one past it; negative, maximum, and the type's overflow or precision edge; duplicates; unicode and length limits on text; the timezone or currency-unit ambiguity. Happy path plus three more happy paths with different numbers is one test wearing four names.
- Choose the smallest honest double. Prefer the real object; then an in-memory fake; then a stub returning a fixed value; a mock that asserts on interactions last, and only when the interaction is the behaviour (an email was sent, a payment was captured). Never double the thing under test. For a real dependency with a real protocol — database, HTTP client, queue — prefer an integration test against the real thing over a stub of it.
- Back every double with a contract check. A stub encodes a belief about what the dependency returns; if nothing ever executes the real thing, the suite proves only that your code parses your own fiction. One integration or contract test per doubled boundary is the minimum price.
- Make it deterministic by construction. Inject the clock and the random source rather than
freezing globals; no live network; no
sleep; no dependence on locale, timezone, filesystem order or map iteration order; no state shared between tests. Seed randomised data and print the seed in the failure output. - Isolate and clean up. Each test creates the data it needs and leaves nothing behind — transaction rollback, a fresh temporary directory, a per-test schema or namespace. Then prove it: run the single test alone, and run the file in a randomised order.
- Test error paths as contracts. Assert the specific type and the identifying part of the message or code the caller branches on. "Something was raised" passes for a typo in the code under test.
- Name the test so the failure report reads as a sentence — subject, condition, expectation. The name is what the next person sees at 3am, before the assertion.
- Check it is not over-specified. Refactor the internals without changing behaviour — rename a private helper, reorder independent calls. If the test goes red, it is pinned to the implementation and will cost more than it catches.
Checklist
- Behaviour stated in the caller's terms before writing the test
- Test observed failing for the intended reason, not just passing
- Assertions on outcomes and state, not on collaborator calls (unless the call is the behaviour)
- One behaviour per test; repeated inputs parameterised rather than copied
- Boundaries enumerated: empty, absent, one, many, limit, limit±1, duplicate, unicode
- Smallest honest double used; nothing doubled that could be real
- Every doubled boundary has at least one test against the real dependency
- Clock, randomness, network, ordering and shared state all controlled
- Passes alone, passes in a randomised order, leaves no residue
- Error cases assert the specific type or code the caller depends on
- Names read as sentences in the failure output
Failure handling
- Test passes when you break the code — it does not test what its name claims. Fix the assertion or delete it; a test that cannot fail is a false green with maintenance cost.
- Test is flaky — do not add a sleep or a retry. Find the source: shared state, real time, real network, ordering, or an actual race in the production code. The last one is a bug, and the flake is the only evidence you will get of it.
- Setup is enormous — that is a design finding, not a test problem. Too many collaborators to construct means too many dependencies; report it rather than building a scaffold that will rot.
- Snapshot or whole-payload assertion — it fails on every unrelated change until someone regenerates it unread. Use one only where the whole output genuinely is the contract, and assert on specific fields everywhere else.
- Test needs credentials or a shared environment — stop before pointing it at anything shared or production-like. Writing to a real system or a shared account is outward-facing: ask first, and prefer a local instance or a disposable record.
- Bug cannot be reproduced in a test — say so explicitly. A fix shipped without a failing test first is a guess; record what was tried and that the regression is unguarded.
Evidence to report
The test command and its actual output, not a summary of it; for each new test, that it was seen red and why; the mutation check (what was broken, that it went red, that it was restored); which boundaries are covered and which inputs are not; which dependencies are doubled and where the real one is exercised instead; and the isolation result — alone and in randomised order. "Tests added and passing" without a red observation says only that code was written.
Signals
- GitHub stars
- 20
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
test-design- Source
- github.com/nahid-sparktales/agent-dispatcher