Skip to content
Back to skills

Test

ASecurity

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-firs...

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 5, 2026
ai-agentsgobashsqltestingrefactoringcode-review

Works with

  • cli

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned September 24, 2026

npx -y skills add mik2win/foureyes --skill test --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Test?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Test
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/mik2win-test/badge)](https://www.skillsdirectory.com/skills/mik2win-test)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: test
disable-model-invocation: true
description: >-
  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).
allowed-tools: Read, Grep, Glob, Bash, Edit, Write, Agent
effort: high
---

# 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`](reference/legacy-mode.md), characterize first.

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

| Layer | Examples | Test approach |
|-------|----------|---------------|
| Pure / domain | calculations, transforms, validators, value objects | Pure unit tests — no mocks, deterministic inputs from factories |
| Service / use-case | commands, queries, orchestration | Mock only the external boundary; test the real domain logic they call |
| Infra / IO | network, DB, filesystem, queues, clock | Mock 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 |
| UI | components, views | Render + 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.

| Unit | Kind | Covered? | Verdict |
|------|------|----------|---------|
| `process_order` | pure | partial | unit-test thoroughly |
| `OrderResult` | value object | yes | — |
| `fetch_remote` | io | no | one 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:

| Unit | Scenario | Expected | Type | Mock boundary |
|------|----------|----------|------|---------------|
| `process_order` | no items | empty result, no charge | unit | none |
| `process_order` | valid items | total computed, 1 charge | unit | none |
| `fetch_remote` | network error | propagates after retry | integration | network client |
| `handle_event` | subscriber callback raises | engine does not propagate | async unit | none |

**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`](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`](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`](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.

| Antipattern | Severity | Fix |
|-------------|----------|-----|
| Test with no assertion / no expected-exception | CRITICAL | Add a real assertion or delete it — it proves nothing |
| Mocks the unit's own domain logic / same-module internals | CRITICAL | Remove the mock; exercise the real implementation |
| Assertions weakened to make a flaky test pass | CRITICAL | Restore the real expectation; fix the code or premise |
| Trivial/always-true assertions (`is not None`, `len > 0`) | CRITICAL | Assert the actual expected value |
| Setup hides the Arrange (shared mutable setup assigning state) | STRUCTURAL | Extract to a fixture or local builder; keep Arrange visible |
| Order-dependent / shared module-level state | STRUCTURAL | Each test builds its own state in Arrange |
| Real sleep / real clock / real network in a unit test | STRUCTURAL | Mock the boundary or mark as integration |
| Hardcoded temp/output paths | STRUCTURAL | Use the framework's temp-dir helper |
| Unseeded randomness | STRUCTURAL | Seed it for repeatability |
| Loop over cases inside one test | STRUCTURAL | Extract to parametrized/table-driven cases |
| Same complex object built inline in many tests | STRUCTURAL | Move to the project's factory location |
| Legacy xUnit assert style where the stack prefers plain asserts | STYLE | Convert 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`](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):

| Principle | How to find violations | Fix |
|-----------|------------------------|-----|
| **Fast** | the runner's slowest-tests report (`--durations`-style flag) | mock at the boundary, use in-memory data, or move it to the integration lane |
| **Independent** | grep the target for class/module-level state assigned outside a test body, and for the framework's module-level setup hooks | build the state inside each test's Arrange |
| **Repeatable** | grep 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-validating** | grep for tests whose only output is printing/logging, with no assertion or expected-exception | every 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`](../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.

Files in this skill

  • SKILL.md16 KB
  • reference/async-patterns.md3.9 KB
  • reference/writing-patterns.md7.2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.