ADR-0020's stated anti-goal is satisfying the size gate by deleting content rather
than relocating it. prototype's logic.md lost three anti-patterns, including
"Don't generalise" — the one with a distinct failure mode, a throwaway growing
abstractions for hypothetical futures, and the one the logic branch is most exposed
to. It survived nowhere in the repo.
The deletion bought nothing measurable: references/ sits outside the body FAIL,
outside the 600-word suggestion and outside the Vale gate, and prototype's body is
483 words. 4011d14 restored the byte-identical defect in the sibling ui.md with
exactly that reasoning in its message and left this file alone. Restored verbatim
from main.
improve-codebase-architecture had dropped "refactoring" from its description
entirely, so "find refactoring opportunities in this repo" had no lexical match,
while spending characters on a boundary against tdd — which cannot plausibly steal
an architecture request. retrofit.md names that exact failure: an invented boundary
costs characters and buys no routing accuracy.
write-docs had dropped all four literal trigger phrasings, leaving them only in the
body and a `when:` field, neither visible to the router at routing time. Its
boundary also sent PRDs to grill-with-docs, which has no PRD flow, and the body
repeated that at two more places. Per #123 nothing in the corpus produces a PRD, so
no target was invented — the boundary is now honest about the ADR case only.
Two READMEs added by this branch contradicted the SKILL.md they document: triage's
label resolution, and grill-with-docs' fifth during-session behaviour. Unconditional
reference pointers in tdd and improve-codebase-architecture are now conditional; the
files stay at the skill root, which is #122's scope.
Refs: #114, #122, #123
ADR: 0020
113 lines
4.5 KiB
Markdown
113 lines
4.5 KiB
Markdown
---
|
|
name: tdd
|
|
description: >
|
|
Use when the user wants a feature built or a bug fixed test-first, in a strict
|
|
red-green-refactor loop, one behaviour at a time. Not diagnosing an existing
|
|
bug -> `diagnose`. Not throwaway exploratory code -> `prototype`.
|
|
---
|
|
|
|
# Test-Driven Development
|
|
|
|
## Philosophy
|
|
|
|
**Core principle**: Tests should verify behavior through public interfaces, not implementation details. Code can change entirely; tests shouldn't.
|
|
|
|
**Good tests** are integration-style: they exercise real code paths through public APIs. They describe _what_ the system does, not _how_ it does it. A good test reads like a specification - "user can checkout with valid cart" tells you exactly what capability exists. These tests survive refactors because they don't care about internal structure.
|
|
|
|
**Bad tests** are coupled to implementation. They mock internal collaborators, test private methods, or verify through external means (like querying a database directly instead of using the interface). The warning sign: your test breaks when you refactor, but behavior hasn't changed. If you rename an internal function and tests fail, those tests were testing implementation, not behavior.
|
|
|
|
If you need worked examples of the difference — a behaviour-level test beside the implementation-coupled version of the same check — read `tests.md`. If a test needs a collaborator faked, read `mocking.md` before reaching for a mock.
|
|
|
|
## Anti-Pattern: Horizontal Slices
|
|
|
|
**DO NOT write all tests first, then all implementation.** This is "horizontal slicing" - treating RED as "write all tests" and GREEN as "write all code."
|
|
|
|
This produces **crap tests**:
|
|
|
|
- Tests written in bulk test _imagined_ behavior, not _actual_ behavior
|
|
- You end up testing the _shape_ of things (data structures, function signatures) rather than user-facing behavior
|
|
- Tests become insensitive to real changes - they pass when behavior breaks, fail when behavior is fine
|
|
- You outrun your headlights, committing to test structure before understanding the implementation
|
|
|
|
**Correct approach**: Vertical slices via tracer bullets. One test → one implementation → repeat. Each test responds to what you learned from the previous cycle. Because you just wrote the code, you know exactly what behavior matters and how to verify it.
|
|
|
|
```
|
|
WRONG (horizontal):
|
|
RED: test1, test2, test3, test4, test5
|
|
GREEN: impl1, impl2, impl3, impl4, impl5
|
|
|
|
RIGHT (vertical):
|
|
RED→GREEN: test1→impl1
|
|
RED→GREEN: test2→impl2
|
|
RED→GREEN: test3→impl3
|
|
...
|
|
```
|
|
|
|
## Workflow
|
|
|
|
### 1. Planning
|
|
|
|
When exploring the codebase, use the project's domain glossary so that test names and interface vocabulary match the project's language, and respect ADRs in the area you're touching.
|
|
|
|
Before writing any code:
|
|
|
|
- [ ] Confirm with user what interface changes are needed
|
|
- [ ] Confirm with user which behaviors to test (prioritize)
|
|
- [ ] Identify opportunities for [deep modules](deep-modules.md) (small interface, deep implementation)
|
|
- [ ] Design interfaces for [testability](interface-design.md)
|
|
- [ ] List the behaviors to test (not implementation steps)
|
|
- [ ] Get user approval on the plan
|
|
|
|
Ask: "What should the public interface look like? Which behaviors are most important to test?"
|
|
|
|
**You can't test everything.** Confirm with the user exactly which behaviors matter most. Focus testing effort on critical paths and complex logic, not every possible edge case.
|
|
|
|
### 2. Tracer Bullet
|
|
|
|
Write ONE test that confirms ONE thing about the system:
|
|
|
|
```
|
|
RED: Write test for first behavior → test fails
|
|
GREEN: Write minimal code to pass → test passes
|
|
```
|
|
|
|
This is your tracer bullet - proves the path works end-to-end.
|
|
|
|
### 3. Incremental Loop
|
|
|
|
For each remaining behavior:
|
|
|
|
```
|
|
RED: Write next test → fails
|
|
GREEN: Minimal code to pass → passes
|
|
```
|
|
|
|
Rules:
|
|
|
|
- One test at a time
|
|
- Only enough code to pass current test
|
|
- Don't anticipate future tests
|
|
- Keep tests focused on observable behavior
|
|
|
|
### 4. Refactor
|
|
|
|
After all tests pass, look for [refactor candidates](refactoring.md):
|
|
|
|
- [ ] Extract duplication
|
|
- [ ] Deepen modules (move complexity behind simple interfaces)
|
|
- [ ] Apply SOLID principles where natural
|
|
- [ ] Consider what new code reveals about existing code
|
|
- [ ] Run tests after each refactor step
|
|
|
|
**Never refactor while RED.** Get to GREEN first.
|
|
|
|
## Checklist Per Cycle
|
|
|
|
```
|
|
[ ] Test describes behavior, not implementation
|
|
[ ] Test uses public interface only
|
|
[ ] Test would survive internal refactor
|
|
[ ] Code is minimal for this test
|
|
[ ] No speculative features added
|
|
```
|