agentleFS
mik2winmik2win/foureyes/skills/test/SKILL.md

test

Skill2 starsChanged 3 months agoMITView on GitHub

Author new tests or refactor existing ones — layer classification, inventory & gap analysis, test-design table, AAA/naming/mock discipline, async safety, antipattern cleanup, and coverage verification. Covers both writing tests from scratch and improving the quality of tests that already exist. TRIGGER when: the user wants to write/add/generate tests for a file, directory, or feature, OR to audit/refactor/clean up existing tests. DO NOT TRIGGER when: the user wants to build NEW code test-first (use /tdd), only wants to run the existing suite, or wants to debug a specific failure (use /diagnose — but come back here to lock the fix with a test).

179 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use agent-context-isolation, agentica-infrastructure and 10 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.

The file

We split this skill into the rules it gives an agent

11 rules. 0 are common, 7 are distinctive, and 4 are very specific to this project.

  • 0

    Common

    Most of the 154 edited versions we read keep these.

  • 7

    Distinctive

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    If $ARGUMENTS is empty, ask what to test and stop.

    line 27
  2. Very specific:

    Use the test command, test layout, and layers from PROJECT.md everywhere below.

    line 51
  3. Distinctive:

    Before writing any fixture, check the project's factory/fixture location (from PROJECT.md / stack rules) — reuse existing builders instead …

    line 111
  4. Distinctive:

    Never mock your own domain logic or same-module internals — that tests the mock, not behavior.

    line 132
  5. Very specific:

    Properties over examples, where an invariant exists: if the unit has a nameable invariant ("output stays sorted", "sums are conserved", …

    line 167
  6. Distinctive:

    Fixture scope: default to per-test (fresh) scope.

    line 175
  7. Distinctive:

    Isolation: no shared mutable module-level state.

    line 180
  8. Distinctive:

    Use the framework's async test setup (per stack rules).

    line 198
  9. Very specific:

    No real sleeps/timers — inject or mock the clock.

    line 204
  10. Distinctive:

    When converting, split merged assertions into focused tests and replace per-case copies with one parametrized test.

    line 234
  11. Distinctive:

    Run via PROJECT.md → Commands — never hardcode commands:

    line 281

“Common” is measured: kept by at least 6 in 10 of the 154 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Test Authoring & Refactoring

Target: $ARGUMENTS — one of:

  • A source file/dir (<src>/foo) → write tests for it.
  • A test file/dir → audit and refactor existing tests.
  • A description ("login error isolation") → write targeted scenarios.
  • staged → audit/cover the test or source files in staged changes.
  • A plan/card file with a scope note → pin the scoped modules, then write the log back into that file (see Persist to the artifact).

If $ARGUMENTS is empty, ask what to test and stop. Work the phases in order; Phases 1–3 are investigative (plan before writing), 4–7 implement, 8 verifies.

Progressive loading. Phases 1–3 and 8 need nothing but this file. Phases 4–7 carry the rules inline and the worked examples in reference/, read only when the target calls for it — each phase below names its file and its short-circuit. A run that writes three synchronous unit tests should never open the async reference.

Phase 0 — Load profile

Make this generic workflow concrete from project facts:

  • Read .claude/PROJECT.md — Stack, Commands (test, test:targeted/test:one), and Architecture (modules/layers, and the test layout: where tests, fixtures, and factories live).
  • Read .claude/rules/_generic/testing.md.
  • If CONTEXT.md exists, read it so test names use the project's domain vocabulary.
  • Read every stack testing rule whose paths frontmatter matches the target. These packs carry the concrete framework, assertion style, fixtures, and factory locations — never hardcode them here.
  • If PROJECT.md is missing or still TEMPLATE, fall back to the root CLAUDE.md (always in context) when it carries the stack, test command, and test layout — note you're running without a kit profile. Only if neither has them, run /bootstrap first.

Use the test command, test layout, and layers from PROJECT.md everywhere below. Apply the stack-specific test patterns from the installed .claude/rules/* — do not invent framework idioms in this skill.


Phase 1 — Classify

Mode:

  • Source file/feature given → new tests (Phase 2, then 4–7).
  • Existing test file/dir given → refactoring (Phase 2, then 6).
  • Both → do both, in that order.
  • Source with no tests and nothing stating what it should do (no spec, ticket, docstring or sibling test) → legacy mode: read reference/legacy-mode.md, characterize first.

Layer of the code under test (use the layers named in PROJECT.md → Architecture):

LayerExamplesTest approach
Pure / domaincalculations, transforms, validators, value objectsPure unit tests — no mocks, deterministic inputs from factories
Service / use-casecommands, queries, orchestrationMock only the external boundary; test the real domain logic they call
Infra / IOnetwork, DB, filesystem, queues, clockMock the boundary for its callers; async-aware where the language has async. Every persistence mapping still gets one real create/read/update/delete round-trip in the integration lane — nothing else catches SQL drifting out of step with the code
UIcomponents, viewsRender + behavior via public output; query by role/text, not internals

The layer decides the strategy: unit (no mocks) vs mock-boundary vs async-aware vs render. Find existing coverage before planning — map the target to its expected test path by the project's naming convention and grep the test tree for references to the units.


Phase 2 — Inventory & gap analysis

For new tests, list every public unit (function, method, class, exported symbol) and mark coverage. For refactoring, list the existing tests and the antipatterns they carry.

UnitKindCovered?Verdict
process_orderpurepartialunit-test thoroughly
OrderResultvalue objectyes—
fetch_remoteionoone integration test

Kind: pure (deterministic, no side effects) · stateful (mutates state) · async · io (network/file/DB/clock).

Verdict — two axes, never a coverage percentage per layer. Complexity/domain significance against the number of collaborators (mutable or out-of-process; values do not count, implicit ones through statics do). High + few → unit-test thoroughly. Low + few → do not test it at all. Low + many → one integration test. High + many → a design defect: report it and route to /refactor before any test.

Evidence before the audit (refactoring mode) — gather with the stack's own idioms (from the installed testing rule / CONTEXT.md), never a hardcoded framework token. Grep the target test tree for:

  • test functions with no assertion / no expected-exception (the stack's assert/raises idiom).
  • real sleeps and real-clock reads (sleep, now, time()), and unseeded randomness.
  • hardcoded temp/output paths instead of the framework's temp-dir helper.
  • mocks that patch the project's own modules/layers (from PROJECT.md → Architecture), not boundaries.
  • legacy xUnit-style setup/teardown/assert calls where the stack prefers plain asserts.

Record each hit as a row that feeds the Phase 6 findings table.

Before writing any fixture, check the project's factory/fixture location (from PROJECT.md / stack rules) — reuse existing builders instead of constructing domain objects inline.

For large or unfamiliar targets, delegate to the test-gap-finder agent to enumerate untested paths and rank them by risk, then design tests for the critical gaps it reports.


Phase 3 — Test design

Plan every test as a row before writing code:

UnitScenarioExpectedTypeMock boundary
process_orderno itemsempty result, no chargeunitnone
process_ordervalid itemstotal computed, 1 chargeunitnone
fetch_remotenetwork errorpropagates after retryintegrationnetwork client
handle_eventsubscriber callback raisesengine does not propagateasync unitnone

Mock decision — five rules:

  1. Mock external boundaries only: network, DB, filesystem, clock/now, sleep.
  2. Never mock your own domain logic or same-module internals — that tests the mock, not behavior.
  3. Several doubles in one test is a report about the design, not a lane to change — name the defect first (skills/tdd/mocking.md §Smell: too many mocks), then widen only if the seam is truly wide.
  4. Prefer a fake over a mock for a collaborator you own but must stand in for (a repository, a queue, a store): a small working in-memory implementation exercises real behavior, where a mock only replays a scripted return and asserts on the script.
  5. A client for an external provider → test the contract from your side in two halves that never call it: reference/writing-patterns.md §Prefer a fake over a mock.

Which scenario gets the integration test. Cover a scenario's edge cases with unit tests and give it one integration test — the longest happy path, crossing the most out-of-process dependencies; add another only to reach a dependency that path misses. A failure that crashes on the first run in any environment and cannot corrupt data needs no test: declare the skip under Remaining Gaps.

Fixture vs local helper: reusable across ≥2 files → shared fixture/setup in the nearest common location; used once → local helper at the top of the file; complex domain object → the project's factory location.


Phase 4 — Writing rules

Load on demand: writing new tests → read reference/writing-patterns.md (worked good/bad shapes for naming, AAA, parametrize, fixture scope, mock discipline, fakes, assertion quality, isolation). Refactor-only runs skip it except its §Refactoring conversions (Phase 6).

  • Naming: a statement of fact about behaviour in domain words (delivery_with_a_past_date_is_invalid); method-name template only for utility code; an enforced stack shape wins. No test_works, test_case_1.
  • AAA: Arrange → Act → Assert, no interleaving. One logical assertion target per test (several assert lines for the same outcome are fine). Never call the unit inside the assertion — store the result first. Delete arrange that the act doesn't use.
  • Parametrize: same logic over ≥2 inputs → one parametrized/table-driven test, not N copy-pasted functions and not a for loop inside one test.
  • Properties over examples, where an invariant exists: if the unit has a nameable invariant ("output stays sorted", "sums are conserved", "encode∘decode = identity" — rules/_generic/core.md), add one property test asserting it across generated inputs — the stack's generator library if PROJECT.md/rules name one (Hypothesis, fast-check, StreamData…), else a seeded loop over randomized + boundary inputs. Example tests pin points; the property pins the region between them. A second trigger: values that must be present but could be swapped for any others mark a property, not an example — state the invariant, generate them, keep the real boundaries as explicit cases.
  • Fixture scope: default to per-test (fresh) scope. Wider scope (module/session) only for expensive read-only setup — never for fixtures that mutate state.
  • Mock discipline: patch at the call site, not the definition site. Assert call count/args only when the side effect (not the return value) is the thing under test. Use the framework's teardown-safe mocking from the stack rules.
  • Isolation: no shared mutable module-level state; each test passes independently and in any order. Filesystem → the framework's temp-dir helper, never hardcoded paths. Randomness → always seed. Time → freeze/inject, never read the real clock in assertions.
  • Assertion quality: assert specific values, not is not None / len > 0. Compare floats with an approx/tolerance helper. For exceptions, assert both type and message (message must contain the offending value). A unit whose only visible effect is a developer-facing log line has no observable behaviour — record a coverage gap, never assert on log prose; logs read by operators or the business are contract, so assert the named event or level+key.

Phase 5 — Async test safety (where the language has async)

Load on demand: the target actually contains async code → read reference/async-patterns.md (async test shapes, the invariant checklist, orphaned-task cleanup, the mocked-clock hang). Short-circuit: a synchronous target skips this phase and that file entirely.

  • Use the framework's async test setup (per stack rules); don't block the loop.
  • Subscriber/callback failure is isolated — the engine/orchestrator must not propagate it.
  • Cancellation propagates: a cancellation signal must never be swallowed by a bare catch-all except/catch.
  • Parallel fan-out: one task's failure must not kill its siblings (collect, don't crash).
  • Ordering invariants: state is persisted before events are emitted to subscribers.
  • No real sleeps/timers — inject or mock the clock; real sleep makes tests slow and flaky.
  • A mocked sleep/clock returns instantly, so an unbounded retry/poll loop under test spins at full speed (a runaway CPU/memory hang). Guard such tests with the framework's per-test timeout and a real termination condition (e.g. a call-count ceiling) — never sleep(0), which is mocked too.

Phase 6 — Refactoring existing tests

Audit the target and produce a severity-ordered findings table. Severities:

  • CRITICAL — false confidence (passes regardless of behavior), crashes, or masks bugs.
  • STRUCTURAL — maintainability/clarity failures that make tests hard to change safely.
  • STYLE — minor; fix while touching the file, not a standalone change.
AntipatternSeverityFix
Test with no assertion / no expected-exceptionCRITICALAdd a real assertion or delete it — it proves nothing
Mocks the unit's own domain logic / same-module internalsCRITICALRemove the mock; exercise the real implementation
Assertions weakened to make a flaky test passCRITICALRestore the real expectation; fix the code or premise
Trivial/always-true assertions (is not None, len > 0)CRITICALAssert the actual expected value
Setup hides the Arrange (shared mutable setup assigning state)STRUCTURALExtract to a fixture or local builder; keep Arrange visible
Order-dependent / shared module-level stateSTRUCTURALEach test builds its own state in Arrange
Real sleep / real clock / real network in a unit testSTRUCTURALMock the boundary or mark as integration
Hardcoded temp/output pathsSTRUCTURALUse the framework's temp-dir helper
Unseeded randomnessSTRUCTURALSeed it for repeatability
Loop over cases inside one testSTRUCTURALExtract to parametrized/table-driven cases
Same complex object built inline in many testsSTRUCTURALMove to the project's factory location
Legacy xUnit assert style where the stack prefers plain assertsSTYLEConvert per stack rules

When converting, split merged assertions into focused tests and replace per-case copies with one parametrized test. Mock only at the external boundary; keep the domain real.

Load on demand: a multi-line conversion (xUnit class style → plain assertions, parametrize extraction) → read reference/writing-patterns.md §Refactoring conversions. A one-line severity fix needs no reference file.

F.I.R.S.T. audit (refactor mode only)

Principle definitions live in rules/_generic/testing.md. What this phase adds is that four of the five are mechanically greppable — run them over the target instead of eyeballing, using the project's own paths and idioms (PROJECT.md → Stack, test layout):

PrincipleHow to find violationsFix
Fastthe runner's slowest-tests report (--durations-style flag)mock at the boundary, use in-memory data, or move it to the integration lane
Independentgrep the target for class/module-level state assigned outside a test body, and for the framework's module-level setup hooksbuild the state inside each test's Arrange
Repeatablegrep for RNG calls without a seed argument, and for reads of the real clock (now, today, monotonic timers)seed the RNG; freeze or inject the clock
Self-validatinggrep for tests whose only output is printing/logging, with no assertion or expected-exceptionevery test asserts, or it proves nothing

Timely is a process principle, not a greppable one: new functions arrive with their tests. It shows up here only as a gap in Phase 2's inventory, never as a finding in an existing file.

Report each hit as a Phase 6 finding with its severity, not as a silent fix — an unseeded RNG in a test that has never flaked is still CRITICAL-adjacent, and the user decides whether it is worth touching now.


Phase 7 — Self-review checklist

  • Every test name states a behaviour fact in domain words (method-name template only for utility code / enforced stack shape).
  • No test without at least one real assertion / expected-exception.
  • No domain logic or same-module internals mocked — boundaries only.
  • Randomness seeded; time frozen/injected; no hardcoded paths; no real sleeps.
  • Floats compared with tolerance; exceptions assert type + message.
  • Reusable fixtures and object builders live in shared/factory locations, not inline.
  • Tests that touch IO boundaries are marked/categorized per stack convention.
  • Mutation thinking: for each critical-path test, name one plausible bug that would still pass it (off-by-one, wrong branch, swallowed error) — a survivor means the assertion is too weak; assert exact values, resulting state, or the side effect itself. Once for the whole suite: the dumbest implementation that still passes every test names a missing case (/tdd).

Phase 8 — Coverage verification

Run via PROJECT.md → Commands — never hardcode commands:

  • Run the targeted test command on the new/changed tests first; all pass.
  • Run the full test command; no new failures.
  • Check coverage for the target if the project defines a coverage command.
  • Write each new test black-box from the requirement it defends, not from the branch coverage pointed at.
  • A gap no requirement defends is a dead-code candidate: propose deleting the branch, not covering it.

Coverage is a floor, not a goal — a passing suite with no real assertions is worthless. Prioritize: correctness of existing tests > coverage of new critical paths > style.


Output

## Test Design — <target>
| Unit | Scenario | Expected | Type | Mock boundary |   (the Phase 3 table)

## Refactoring Actions   (omit if target = new code)
| Antipattern | File:Line | Severity | Fix |

## Files
- written:  <paths>
- changed:  <paths>

## Test Run
<targeted result> · <full-suite result> · <coverage before→after if available>

## Remaining Gaps
<units/paths still untested and why, or "none">

Persist to the artifact

(Mandatory when $ARGUMENTS or the session brief names a plan, card or spec file.) Follow ../implement/reference/work-log.md. Keep a ledger on disk from the first phase and record deviations the moment they are decided. Before the final reply, append ## Test Log — <date> — <verdict> to that file; its ### Result is the pins written (test → behaviour pinned), defects found while pinning (pinned as current behaviour) with a suggested owner, coverage before → after if measured, and any floors or budgets changed. Anything you could not observe yourself goes under Re-check later, with its command and today's baseline: a post-deploy log grep, a soak count, a UI gesture. The chat output is a copy of the log, never the only record. No planning artifact → skip this section.

See also

  • test-gap-finder agent — enumerate and rank untested paths before designing tests.
  • /tdd — build NEW code test-first (red → green → refactor); this skill covers/refactors tests for code that already exists.
  • /code-review — quality pass on the new tests and the code under test.
  • /diagnose — for a reported bug, root-cause it there; reproduce with a failing test first, then return here to lock the fix in.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.