| name | tdd |
| description | Interactive guide for Detroit School TDD. Use whenever the user asks to write or modify code - new feature, bug fix, debug, refactoring, modification. Activates a RED-GREEN-REFACTOR workflow with validation at each step. |
TDD Interactive Guide - Detroit School
Persona
Read .claude/roles/senior-dev.md — adopt this profile and follow all its rules.
Detroit School Philosophy
State-based testing: We test the observable result, not the internal interactions.
| Principle | Explanation |
|---|
| Test state | Verify the final result, not how we got there |
| Inside-Out | Start from the domain, work outward |
| Minimal mocks | Only for external I/O (gateways, API, DB) |
| Robust tests | Resistant to internal refactoring |
When to mock:
- Gateways (API, database, file system)
- External services (email, notifications)
- Internal business logic
- Collaborations between domain objects
it("should enqueue a review job", () => {
const queue = new ReviewQueue();
queue.enqueue({ mrId: "mr-42", platform: "gitlab" });
expect(queue.pending).toHaveLength(1);
expect(queue.peek()?.mrId).toBe("mr-42");
});
it("should call dispatcher.notify", () => {
const dispatcher = mock<JobDispatcher>();
queue.enqueue({ mrId: "mr-42", platform: "gitlab" });
expect(dispatcher.notify).toHaveBeenCalled();
});
ReviewFlow example (project-relevant test):
describe("ReviewScore", () => {
it("should create a valid review score", () => {
const score = createReviewScore(8);
expect(score.value).toBe(8);
expect(score.label).toBe("good");
});
it("should reject a score below 0", () => {
const result = createReviewScore(-1);
expect(result).toBeNull();
});
it("should reject a score above 10", () => {
const result = createReviewScore(11);
expect(result).toBeNull();
});
});
TDD Manifesto
TDD follows these fundamental principles:
| Principle | Meaning |
|---|
| Baby steps | Small steps for fast and regular feedback |
| Continuous refactoring | We improve now, not "later" |
| Evolutionary design | We develop what is necessary and sufficient |
| Executable documentation | Tests ARE the living documentation |
| Minimalist code | Simple and functional > over-dimensioned |
Minimal Test Principle
"The simplest thing that could possibly work." — Kent Beck
"As the tests get more specific, the code gets more generic." — Robert C. Martin
Rules:
- One behavior per test
- From naive to complete: simple case first, edge cases after
- No anticipation: one cycle at a time
Typical progression:
Cycle 1: "should create a completion" (nominal case)
Cycle 2: "should have a rating" (property)
Cycle 3: "should reject rating below 1" (validation)
Cycle 4: "should reject rating above 5" (validation)
Anti-pattern:
it("should validate a completion with rating 1-5 and ISO datetime", () => {
})
it("should create a completion", () => { ... })
it("should reject rating below 1", () => { ... })
Transformation Priority Premise (Robert C. Martin)
The Transformation Priority Premise guides the RED → GREEN transition. When writing minimal code to pass a test, we apply transformations — and these transformations have a natural priority order.
Always choose the highest transformation in the list (the simplest). Skipping steps often leads to suboptimal design.
Transformation list (simplest to most complex)
| Priority | Transformation | Description | Example |
|---|
| 1 | {} → nil | No code → return null/undefined | return undefined |
| 2 | nil → constant | Return a hardcoded value | return [] |
| 3 | constant → variable | Replace constant with parameter | return items instead of return [] |
| 4 | unconditional → conditional | Add a branch | if (age < 18) return false |
| 5 | scalar → collection | Go from single value to array | string → string[] |
| 6 | statement → recursion/iteration | Loop over the collection | items.map(transform) |
| 7 | value → mutated value | Transform the accumulated value | items.reduce(accumulate) |
When to apply
The TPP is a guide, not an absolute rule. It is particularly useful when:
- Going GREEN seems to require "a lot of code at once" → sign that we're skipping transformations
- We hesitate between implementations → choose the highest transformation
- Working on algorithmic logic (parsers, calculations, data transformations, complex validations)
Less relevant for simple orchestration code (call a gateway and return the result).
Concrete example — a presenter formatting tags
// RED: test "should return empty list when no tags"
// GREEN via {} → constant:
return [];
// RED: test "should return formatted tag for single item"
// GREEN via constant → variable:
return [formatTag(tags[0])];
// RED: test "should return formatted tags for multiple items"
// GREEN via scalar → collection + iteration:
return tags.map(formatTag);
If we had jumped directly to map() on the first test, it works — but we would have guessed the design instead of discovering it through tests.
Warning signal
If during GREEN phase you end up writing more than 3-5 lines of code, you're probably skipping transformations. Options:
- Go back and add a simpler intermediate test
- Check which transformation you're applying in the list
Activation
This skill activates whenever the user asks to touch code:
- New features: "Implement...", "Add...", "Create..."
- Bug fixes: "Fix...", "Correct...", "Repair..."
- Debug: "Why does...", "It doesn't work..."
- Modifications: "Modify...", "Change...", "Update..."
- Refactoring: "Refactor...", "Improve...", "Clean up..."
Mandatory Workflow
At each cycle, follow these 3 phases with stop and user validation between each.
Phase RED
Goal: Write ONE failing test
Actions:
- Announce: "RED: I will test [specific behavior]"
- Identify the smallest possible test (baby step)
- Propose the test WITHOUT writing it
- Wait for validation
- Write the test after validation
- Run
yarn test:run to confirm failure
- Ask: "The test fails as expected. Move to GREEN?"
Template:
RED - Test Proposal
Behavior to test: [description]
File: [path]
Proposed test:
[test code - state-based, verifies the result]
This test verifies that [explanation of expected state].
Validate this test?
Phase GREEN
Goal: Make the test pass with MINIMAL code
Actions:
- Announce: "GREEN: I will make the test pass with minimum code"
- Propose the minimal implementation WITHOUT writing it
- Wait for validation
- Write the code after validation
- Run
yarn test:run to confirm success
- Ask: "The test passes. Refactor or next cycle?"
Rules:
- MINIMAL code that makes the test pass
- No premature optimization
- Hardcoded values accepted if sufficient
Template:
GREEN - Implementation Proposal
To make the test pass, I propose:
[minimal code]
This is intentionally minimal because [explanation].
Validate this implementation?
Phase REFACTOR
Goal: Simplify without changing behavior
Principles:
- KISS: The simplest solution
- YAGNI: Remove what is not necessary
- DRY: Factor out only if there is real duplication
Actions:
- Announce: "REFACTOR: Analyzing simplification opportunities"
- Look for: dead code, premature abstractions, accidental complexity
- Propose refactorings one by one
- Wait for validation for each
- Run
yarn test:run after each refactoring
- Ask: "Refactor complete. Next RED cycle?"
Priority order: Remove > Simplify > Reorganize
Template:
REFACTOR - Analysis
Code smells detected:
- [smell 1]: [explanation]
Proposed refactoring:
[description of change]
Apply this refactoring?
Special Case: Debug / Bug Fix
- Understand: Expected behavior vs actual
- RED: Test that reproduces the bug (must fail)
- GREEN: Fix so the test passes
- REFACTOR: Clean up if necessary
The test becomes a protective regression test.
Mandatory Checkpoints
Never move to the next phase without:
- Explicit user validation
- Tests executed and result confirmed
Specs and tickets: no predetermined code
TDD relies on progressive design discovery. Specs/tickets must NOT contain:
| Forbidden | Why |
|---|
| Test names | The test emerges from the need, not the other way around |
| File names | Architecture reveals itself through iteration |
| Method signatures | Design comes from the simplest code |
| Installation commands | Dependencies come when necessary |
Principle: We don't predict the final code. We proceed by baby steps, from the most naive implementation to the final case.
Ticket with "Tests to implement: should_create_user_with_email"
Ticket with "Files: src/services/user.service.ts"
Ticket with "Method: createUser(email: string): User"
Ticket with Gherkin criteria describing the expected BEHAVIOR
The RED-GREEN-REFACTOR cycle makes the design emerge. The ticket describes the WHAT (behavior), not the HOW (implementation).
Anti-patterns to Block
- Production code without a red test
- Multiple tests at once
- Implementing more than necessary in GREEN
- Refactoring without green tests
- Skipping a phase without validation
- Mocking internal business logic
- Predetermining code in specs/tickets
End of Cycle
Cycle Complete
Tests added: [number]
Behaviors covered: [list]
Next suggested behavior: [suggestion]
Continue?