Test-Driven Development (Hybrid Edition)
Overview
Core principle: Tests verify behavior through public interfaces, not implementation. If you didn't watch the test fail, you don't know if it tests the right thing.
Hybrid approach: Superpowers discipline (Iron Law, Delete means delete) + MattPocock intelligence (Planning, Domain awareness, Vertical slicing).
Scope Triage
Before starting, classify the work:
| Signal | Voie | Pipeline |
|---|---|---|
| Bug fix isolé OU fonction simple <20 lignes OU <3 behaviors | LIGHT | Skip Planning → Tracer Bullet → Loop |
| Feature ≥3 behaviors OU touche architecture OU nouveau module | FULL | Planning → Tracer Bullet → Loop → Refactor avancé |
LIGHT: 1 question ("Behaviors à tester ?"), discipline stricte, refactor basique
FULL: Planning complet, domain awareness, deep modules
En cas de doute → LIGHT, puis upgrade to FULL si complexité émerge.
The Iron Law
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
Write code before the test? Delete it. Start over.
No exceptions:
- Don't keep it as "reference"
- Don't "adapt" it while writing tests
- Don't look at it
- Delete means delete
Thinking "skip TDD just this once"? Stop. That's rationalization.
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
Correct approach: Vertical slices via tracer bullets. One test → one implementation → repeat.
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 LIGHT
For bug fixes, simple functions (<20 lines), or <3 behaviors.
1. Quick Planning
Ask user: "Which behaviors should we test?" (prioritize — can't test everything)
2. Tracer Bullet
Write ONE test that confirms ONE thing end-to-end:
RED: Write test for first behavior → test fails
Verify RED (mandatory)
GREEN: Write minimal code to pass → test passes
Verify GREEN (mandatory)
3. Incremental Loop
For each remaining behavior:
RED - Write Failing Test
One minimal test showing what should happen.
const result = await retryOperation(operation);
expect(result).toBe('success'); expect(attempts).toBe(3); });
Clear name, tests real behavior, one thing
</Good>
<Bad>
```typescript
test('retry works', async () => {
const mock = jest.fn()
.mockRejectedValueOnce(new Error())
.mockRejectedValueOnce(new Error())
.mockResolvedValueOnce('success');
await retryOperation(mock);
expect(mock).toHaveBeenCalledTimes(3);
});
Vague name, tests mock not code
Requirements:
- One behavior
- Clear name
- Real code (no mocks unless unavoidable)
Verify RED - MANDATORY
npm test path/to/test.test.ts
Confirm:
- Test fails (not errors)
- Failure message is expected
- Fails because feature missing (not typos)
Test passes? You're testing existing behavior. Fix test.
GREEN - Minimal Code
Write simplest code to pass the test. Don't add features, refactor other code, or "improve" beyond the test.
Verify GREEN - MANDATORY
npm test path/to/test.test.ts
Confirm:
- Test passes
- Other tests still pass
- Output pristine (no errors, warnings)
Test fails? Fix code, not test.
4. Refactor (Basique)
After all tests pass:
- Remove duplication
- Improve names
- Run tests after each change
Keep tests green. Don't add behavior. Never refactor while RED.
Workflow FULL
For features ≥3 behaviors, architectural changes, or new modules.
Phase 0: Planning
Before writing any code:
Explore codebase
- Read
CONTEXT.mdfor domain glossary - Read ADRs in area you're touching
- Understand existing patterns
- Read
Confirm with user
- What interface changes are needed?
- Which behaviors to test? (prioritize — can't test everything)
- Identify opportunities for deep modules (small interface, deep implementation)
- List 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?"
Phase 1: Tracer Bullet
Write ONE test that confirms ONE thing end-to-end:
RED: Write test for first behavior → test fails
Verify RED (mandatory)
GREEN: Write minimal code to pass → test passes
Verify GREEN (mandatory)
This proves the path works end-to-end.
Phase 2: Incremental Loop
For each remaining behavior, follow LIGHT workflow (RED → Verify RED → GREEN → Verify GREEN).
Phase 3: Refactor (Avancé)
After all tests pass, look for refactor candidates:
- Extract duplication
- Deepen modules — move complexity behind simple interfaces
- Apply SOLID principles where natural
- Improve names
- Consider what new code reveals about existing code
- Run tests after each refactor step
Deep modules: Small interface, deep implementation. Good module has rich functionality accessible through simple API. Bad module exposes complexity.
Never refactor while RED. Get to GREEN first.
Common Rationalizations
| Excuse | Reality |
|---|---|
| "Too simple to test" | Simple code breaks. Test takes 30 seconds. |
| "I'll test after" | Tests passing immediately prove nothing. |
| "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" |
| "Already manually tested" | Ad-hoc ≠ systematic. No record, can't re-run. |
| "Deleting X hours is wasteful" | Sunk cost fallacy. Keeping unverified code is technical debt. |
| "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. |
| "Need to explore first" | Fine. Throw away exploration, start with TDD. |
| "Test hard = design unclear" | Listen to test. Hard to test = hard to use. |
| "TDD will slow me down" | TDD faster than debugging. Pragmatic = test-first. |
| "Manual test faster" | Manual doesn't prove edge cases. You'll re-test every change. |
| "Existing code has no tests" | You're improving it. Add tests for existing code. |
Red Flags - STOP and Start Over
If ANY of these present, delete code and restart with TDD:
- Code before test
- Test after implementation
- Test passes immediately
- Can't explain why test failed
- Tests added "later"
- Rationalizing "just this once"
- "I already manually tested it"
- "Tests after achieve the same purpose"
- "It's about spirit not ritual"
- "Keep as reference" or "adapt existing code"
- "Already spent X hours, deleting is wasteful"
- "TDD is dogmatic, I'm being pragmatic"
- "This is different because..."
Example: Bug Fix
Bug: Empty email accepted
RED
test('rejects empty email', async () => {
const result = await submitForm({ email: '' });
expect(result.error).toBe('Email required');
});
Verify RED
$ npm test
FAIL: expected 'Email required', got undefined
GREEN
function submitForm(data: FormData) {
if (!data.email?.trim()) {
return { error: 'Email required' };
}
// ...
}
Verify GREEN
$ npm test
PASS
REFACTOR Extract validation for multiple fields if needed.
Verification Checklist
Before marking work complete:
- Every new function/method has a test
- Watched each test fail before implementing
- Each test failed for expected reason (feature missing, not typo)
- Wrote minimal code to pass each test
- All tests pass
- Output pristine (no errors, warnings)
- Tests use real code (mocks only if unavoidable)
- Edge cases and errors covered
- FULL only: Test names use domain glossary vocabulary (from CONTEXT.md)
- FULL only: Tests respect ADRs in area touched
Can't check all boxes? You skipped TDD. Start over.
Integration with Ecosystem
This skill integrates with:
/setup-matt-pocock-skills— configures CONTEXT.md + ADRs location (required for FULL workflow)/grill-with-docs— builds domain glossary we use for test names/to-issues— creates vertical slices we implement with TDD/improve-codebase-architecture— uses tests as safety net for refactoring/spec-driven— generates spec before invoking this skill
Typical workflow:
/spec-driven → /tdd-hybrid → implementation with tests
Final Rule
Production code → test exists and failed first
Otherwise → not TDD
No exceptions without user's explicit permission.