---
name: Test-Driven Development
slug: test-driven-development-13
category: Quality
description: Test-Driven Development writes code through a test-first red-green-refactor loop with an observed-failure gate. Use it to implement features behavior by behavior from tests, including TPP or ZOMBIES ordering.
github: "https://github.com/testdouble/han/tree/main/han-coding/skills/tdd"
language: Shell
stars: 253
forks: 22
install: "npx degit https://github.com/testdouble/han/tree/main/han-coding/skills/tdd ~/.claude/skills/tdd"
installs_to: ~/.claude/skills/tdd
source_path: han-coding/skills/tdd/SKILL.md
collection_size: 25
category_size: 1418
collection_url: "https://dirskills.com/collections/testdouble/han"
added: 2026-09-02T05:20:51.984Z
last_synced: 2026-09-02T05:20:51.984Z
canonical_url: "https://dirskills.com/skills/test-driven-development-13"
---

# Test-Driven Development

Test-Driven Development writes code through a test-first red-green-refactor loop with an observed-failure gate. Use it to implement features behavior by behavior from tests, including TPP or ZOMBIES ordering.

**Install:**

```bash
npx degit https://github.com/testdouble/han/tree/main/han-coding/skills/tdd ~/.claude/skills/tdd
```

## README

## Project Context

- git installed: !`which git 2>/dev/null || echo "not installed"`
- current branch: !`git branch --show-current 2>/dev/null || echo unknown`
- CLAUDE.md: !`find . -maxdepth 1 -name "CLAUDE.md" -type f`
- project-discovery.md: !`find . -maxdepth 3 -name "project-discovery.md" -type f`
- personal config directory: !`bash "${CLAUDE_PLUGIN_ROOT}/scripts/han-config-dir.sh" 2>/dev/null || echo "$HOME/.claude"`
- project .han/config.md: !`cat .han/config.md 2>/dev/null || echo ""`

As your first action, use the Read tool on `.han/config.md` inside the `personal config directory` path above. A read
that returns no file is no personal configuration: continue silently. When that file or the `project .han/config.md`
probe supplies content, apply it per [config-rule.md](../../references/config-rule.md), which governs precedence
between the two files, relative-path resolution, and what to do with a file that reads but cannot be used.

## Constraints (read before anything else)

This skill writes production and test code in your working tree. It is an execution skill, not a document generator.
These constraints shape every step and override any instinct to move faster.

- **The observed-failure gate is load-bearing.** No production-code change until a test has been run and observed to
  fail for the intended reason in this loop. A test that passes on first run is a stop-and-diagnose signal, not
  progress. This single rule is what separates real TDD from TDD-flavored code. The verbatim Three Laws and Canon TDD
  steps it derives from are in [references/tdd-loop.md](./references/tdd-loop.md); pull that reference when a step needs
  the canon or the implementation gears.
- **The scope gate is the observed-failure gate's companion.** The observed-failure gate proves a red is genuine. It
  does not prove the test deserved to exist in this build. No production-code change outside the scope boundary recorded
  in Step 1, and least of all in shared or cross-application code other consumers depend on. A list item whose green
  requires an out-of-scope edit is a stop, never an implement; Step 2 carries the resolution ladder that Step 3 works.
- **Two hats.** Never refactor while any test is red. See [references/tdd-loop.md](./references/tdd-loop.md) for the
  canonical statement.
- **One behavior at a time.** Exactly one test list item becomes one runnable test per loop. Newly discovered scenarios
  are written to the list and deferred, never implemented in the current loop.
- **BDD framing.** Tests describe observable behavior, named in the project's existing test-naming convention, asserting
  outcomes through the public interface — never private state. The behavior-naming and Given/When/Then protocol is in
  [references/bdd-framing.md](./references/bdd-framing.md); pull it when Step 2 needs it.
- **You will be tempted to fake this.** The specific ways an agent fakes TDD, and the discipline that catches each, are
  in [references/failure-modes.md](./references/failure-modes.md); pull it when a loop feels off (a test passes on first
  run, no red is shown, the implementation has outrun the test, refactor is being skipped).
- **YAGNI governs the refactor step and the test list.** Apply the rule in
  [../../references/yagni-rule.md](../../references/yagni-rule.md): remove duplication, but do not add abstractions,
  configuration, or indirection without evidence. Speculative structure added "for flexibility" during refactor is a
  YAGNI candidate. Speculative scenarios on the test list are deferred with a reopen trigger, never silently added.

# Test-Driven Development

## Step 1: Resolve Project Config and Confirm Scope

**Resolve commands.** Read CLAUDE.md's `## Project Discovery` section for the test command (under
`### Commands and Tests`, not `### Frameworks and Tooling`), the lint command, the build command, language, and
framework. If absent, fall back to `project-discovery.md`. If still absent, run
`${CLAUDE_SKILL_DIR}/scripts/detect-tdd-context.sh` and parse its output for git state and manifest-inferred commands.
Store the resolved test, lint, and build commands for use in every later step.

**Resolve standards and decisions.** Resolve the coding-standards directory and ADR directory the same way: read
CLAUDE.md's `## Project Discovery` section; fall back to `project-discovery.md`; fall back to Glob defaults (`docs/`,
`docs/adr/`, `docs/coding-standards/`, `docs/decisions/`). Also check `CLAUDE.md` and `AGENTS.md` for inline standards.
**Read the standards and ADRs whose titles, paths, or one-line summaries indicate they govern the area being built. Cap
at five documents; if more than five look relevant, list them and read only the five with the strongest apparent
relevance — defer the rest until refactor surfaces a need.** These govern the green and refactor steps. If none exist,
state that plainly and plan to infer conventions from the surrounding code instead.

**Resolve the scope boundary.** Name, in files and directories, what this work is allowed to change, because the scope
gate tests every candidate edit against it. Inside the boundary: the files, directories, or module the request names,
plus the tests that cover them. Outside it: everything the named code merely reaches, meaning shared libraries, engines,
packages, and any code a second application or consumer also uses, plus code another team owns per `CODEOWNERS`. When
the request names no files, take the application or package the requested behavior lives in as the boundary and treat
its dependencies as outside it. Record the boundary; you will test list items and production edits against it.

**Report scope, then proceed (no gate).** This skill runs autonomously after the initial request: it does not stop for
confirmation. State to the user, in a few lines: the behavior or feature to be built, **whether this is net-new behavior
or a fix to existing broken behavior** (a reported bug, a failing case, a fix being driven back in after `/investigate`,
or code that already exhibits the error — recognize the fix case from those signals, not only from the word "bug"), the
scope boundary you just recorded, the resolved test/lint/build commands, the standards and ADRs found (or that none
were), the current branch, and that the skill will now write code in a red-green-refactor loop. If `current branch` from
Project Context is the repository's default branch (`main` or `master`), recommend working on a branch, but do not wait
for an answer. This is a report the user reads while the work runs, not a gate. Continue immediately to Step 2 without
waiting for a response.

**The one exception.** If the initial request or the provided context explicitly states the human wants to review,
verify, or approve the plan or test list before implementation, then this becomes a gate: build the test list in Step 2,
present it together with this scope report, and wait for approval before starting the Step 3 loop. Absent an explicit
request like that, the skill runs to completion without further human input.

Two things can still block a run, both hard dependencies rather than discretionary checkpoints. A missing test command
is the first: if it could not be resolved from CLAUDE.md, `project-discovery.md`, the discovery script, or manifest
inference, ask the user for it, because TDD is impossible without a way to run tests. Exhaust inference before asking.
The second is the top rung of the scope gate's resolution ladder in Step 2, reached only when the requested behavior
cannot be delivered without an out-of-scope change.

## Step 2: Build the BDD Test List

Turn the requested feature or behavior into a test list (Kent Beck's "test list" pattern). Each item is one observable
behavior, phrased as a behavior sentence, not as an implementation note. "Returns the unrounded fee for a sub-dollar
charge" is a list item; "use a BigDecimal" is not. Follow [references/bdd-framing.md](./references/bdd-framing.md) for
how to phrase and name behaviors, and which test-naming convention to adopt (the project's existing convention and any
discovered coding standard win over a literal "should" default).

**Fixing existing broken behavior is a regression test, not a bug-asserting test.** When the work fixes broken behavior,
the list item names the _desired correct_ behavior, not the current broken one: "returns the rounded total for a refund"
(red now because the bug is present, green once the fix lands), never "raises ArgumentError on a refund" — a test that
asserts the error the bug produces passes while the bug is present and breaks when you fix it, locking the bug in. The
regression test asserts what the code _should_ do. The boundary: asserting that the code raises is the _correct_ test
when raising is the specified desired behavior (raise on invalid input); it is wrong only when the raised error _is the
bug being fixed_.

Order the list outside-in by user value: the next item is the most important thing the system does not yet do. When one
behavior expands into several candidate tests (the empty case, the single case, the many case, the boundaries), order
those tests simplest-first — Zero → One → Many — so each test forces the smallest generalization of the code. The
ranking behind that order is in [references/test-selection.md](./references/test-selection.md); pull it when the order
is not obvious. For an item that is **user-observable behavior at a system boundary**, write the outer acceptance test
for it _first_ (it will be red until its inner behaviors exist) and record it as the outer loop for that item. For
internal or utility behavior with no meaningful system boundary, the outer acceptance test is optional; the inner loop
alone is correct.

Apply YAGNI to the list itself. A scenario earns a place only with evidence it is needed now (a user-described need, a
named dependency, an existing code path that breaks, a regulation, a real incident). Scenarios that fail the evidence
test go to a deferred list with the trigger that would reopen them. Do not pad the list for symmetry or completeness.

**Then apply the scope gate to the list.** YAGNI asks whether a behavior has evidence it is needed. The scope gate asks
a question no amount of evidence answers: would making this test pass require changing a file outside the Step 1
boundary? Ask it of every item, and ask it hardest of items that arrived from a test plan, an analysis report, or an
agent finding carrying a severity label. A CRIT or HIGH label is evidence the finding is real. It is never evidence the
fix belongs to this ticket, and an item whose own text names a production change as a prerequisite ("this requires
first adding an explicit `order`") is that production change wearing a test's clothes.

**The resolution ladder.** Work it in order and stop at the first rung that resolves the item. Never skip to
implementing the out-of-scope change.

1. **Redesign the test.** Most items that trip the gate are asking production code to supply something the test could
   arrange for itself. Rebuild the setup so the assertion holds without the out-of-scope behavior, and the item stays on
   the list in its rewritten form. A test that needs the out-of-scope behavior only to make a fixture deterministic
   always resolves here: that is a test-design problem, and leaning on a production change to make it disappear is the
   wrong direction of dependence.
2. **Defer the item as its own work.** When the test cannot be redesigned, move it off the list and write it up as a
   ticket: the behavior, the file that would have to change, who else consumes that file, and the change it needs.
   Report it in Step 5 as work this build did not own. The finding stays alive; it just stops being this build's job.
3. **Escalate.** Only when the requested behavior cannot be delivered at all without the out-of-scope change, stop and
   ask the user. Name the file, its other consumers, the change it needs, and the two ways forward: widen this build's
   scope to include it, or split it into separate work and drop the dependent behavior from this build. This is the one
   rung that pauses an otherwise autonomous run, and it is a hard dependency, not a review checkpoint.

Report the test list to the user, along with any item the scope gate moved and which rung resolved it. Unless the
verify-plan exception from Step 1 applies, continue to Step 3 immediately without waiting for approval. When that
exception applies, present the test list together with the Step 1 scope report and wait for approval before entering the
loop.

## Step 3: The Red-Green-Refactor Loop

Pick exactly one item from the list. Choose one that teaches you something and that you are confident you can implement
in one cycle (Beck's "one step test"). When several items qualify, prefer the one whose passing requires the simplest
transformation of the code: a test needing only a constant return comes before one forcing a conditional, and a
conditional before a loop — the Transformation Priority Premise, made concrete by the ZOMBIES ordering, both in
[references/test-selection.md](./references/test-selection.md); pull that reference when the choice is not obvious. If
every remaining item forces a big leap (a loop or recursion with no smaller test in between), a simpler test is missing:
add it to the list and pick it. Then run these three phases in order. Do not collapse them.

**Read once, don't reread.** Within a single loop iteration, do not reread a file you have already read in this
iteration unless you have edited it. When `grep` returns a line number, use `Read` with `offset` and `limit` to read
20-40 lines around the target — not the entire file. Rereading whole source files between Red and Green of the same
behavior is overhead, not discipline.

### Red

Write exactly one test for the chosen behavior. Name it for the behavior in the project's convention. Assert an
observable outcome through the public interface (Given = arrange the state before; When = the one action under test;
Then = assert the observable result). Write no more of the test than is sufficient to fail; a compilation failure is a
failure.

**Before you run it, check the assertion direction for a fix to broken behavior.** The assertion must target the desired
correct result, so the red you are about to observe is "correct behavior not yet produced" — not "the error the bug
raises was raised successfully." A test that asserts the buggy behavior either passes immediately or goes red for the
wrong reason; both look like a satisfied gate and both lock the bug in. (Asserting a raise is still correct when raising
is the specified desired behavior; the trap is asserting the error that _is_ the bug.)

Run the resolved test command directly with Bash. **Paste the failing assertion plus enough surrounding output (5-10
lines) to confirm the failure reason** — the assertion text or the missing symbol you expect, not an unrelated error. If
the test passed on its first run, paste only the runner's summary line and stop to diagnose: the observed-failure gate
has tripped.

If the test passes on its first run, the observed-failure gate has tripped. Stop. Diagnose one of three causes: the test
is not exercising the behavior; the behavior already exists; or — for a fix to broken behavior — the test is asserting
the current broken behavior (the error the bug raises), which passes precisely because the bug is still present. If the
behavior already exists, cross the item off and pick the next one. If the test is asserting the bug, **do not cross the
item off** — rewrite it to assert the desired correct behavior, so it goes red until the fix lands. Do not write
production code off an unobserved red.

**With the red observed, check where green would land.** Name the files you would edit to make this test pass, before
you edit any of them. If one sits outside the Step 1 boundary, the scope gate has tripped: do not edit it, and work the
resolution ladder from Step 2 instead. A genuine red says the behavior is missing. It does not say this build owns
producing it, and this is the only check that asks. Shared or cross-application code is where the gate matters most,
because the blast radius of an edit there reaches consumers nobody in this build is testing.

### Green

Write the minimum production code that makes this one test pass. Use the smallest gear that works: Obvious
Implementation when you are certain, Fake It (return a constant, generalize later) when you are not, Triangulate (force
the abstraction with a second example) only when you are really unsure. Gears are described in
[references/tdd-loop.md](./references/tdd-loop.md).

While going green, respect the coding standards and ADRs that govern _correctness and architectural placement_: where
this code is allowed to live, which boundary or client it must go through, which contract it must honor. Violating an
ADR boundary is not a sin you clean up later — it is the wrong code. Do **not** apply stylistic or structural polish
here (naming sweeps, extraction, formatting passes). That is the refactor hat, and wearing it now violates "no more code
than is sufficient to pass the test."

Run the full test suite with Bash. **Paste the runner's summary line (pass and fail counts).** Paste full output only if
a previously passing test broke or something unexpected appears. The gate to leave green is: the new test passes and
every previously passing test still passes. If a prior test broke, you are not green — fix it before refactoring.

### Refactor (non-skippable)

Only with every test green. Neglecting this step is the most common way to ruin TDD, so it is not optional: either you
change something, or you state explicitly "no duplication, structure, or standards issue this cycle" and move on.

Eliminate the duplication you just created. Bring the code into full conformance with the resolved coding standards and
ADRs — this is the home for the stylistic and structural standards you deliberately skipped in green.

Apply YAGNI per [../../references/yagni-rule.md](../../references/yagni-rule.md): remove duplication, do not add
speculative abstraction. Defer speculative structure with the trigger that would reopen it; never add silently, never
drop silently.

Change no behavior. Re-run the full suite after the refactor. **Paste the runner's summary line** — paste full output
only if something unexpected appears. The suite must stay green. If a refactor reddened a test, revert it — a refactor
that changes behavior is a defect, not a refactor.

### Close the cycle

Cross the completed item off the list. Append any scenarios you discovered while implementing (deferred, with their
reopen trigger if speculative), but do not implement them now. If the open list has grown past roughly ten items, do not
stop for input: flag it prominently as a scope warning, keep going, and record in the final summary that the work
exceeded the recommended size and should be split next time. A runaway list is a scope signal, not a reason to pause for
a human.

**Running collaboratively.** When the request asks to review each behavior as it lands, which is what `pairing` does
when it hands work here, stop at this point and hand control back instead of continuing. Present the stop in the shape
[collaborative-stop-rule.md](../../references/collaborative-stop-rule.md) specifies. Absent such a request, continue as
below; an ordinary invocation is unchanged.

Return to the top of Step 3 with the next item. Continue until the list is empty.

## Step 4: Close the Outer Loop

For any item that had an outer acceptance test (Step 2), run that test now. It should pass only because its inner
behaviors are all implemented with real code (not mocks). If it is still red, the gap is a missing inner behavior: add
the missing scenario to the test list and return to Step 3. The acceptance test going green is the signal the
user-facing behavior is actua
