| name | tdd |
| description | Enforces test-driven development (TDD) following Kent Beck's methodology. MANDATORY micro-cycle approach - write ONE test, watch it fail, write minimal implementation, refactor, then NEXT test. NEVER write all tests first or implementation first. Use for ANY code writing task. |
| allowed-tools | Read, Glob, Grep, Bash(bun test:*), Bash(node --import tsx --test:*), Write, Edit |
Test-Driven Development (TDD) Enforcement
CRITICAL RULE: ONE Test at a Time
NEVER write implementation code before writing a test. NEVER write multiple tests before writing implementation.
The cycle is: ONE test → implementation for that test → refactor → REPEAT
When the user asks you to implement ANY code (features, bug fixes, new functions, deciders, projections, handlers), you MUST follow this EXACT sequence:
- Write ONE test (just one!)
- Run test → verify it FAILS (Red phase)
- Write MINIMAL implementation to make THAT test pass (Green phase)
- Run test → verify it PASSES
- Refactor if needed while keeping tests passing (Refactor phase)
- Run test → verify still passing
- GOTO step 1 for the NEXT test
You are FORBIDDEN from:
- ❌ Writing all tests first, then all implementation
- ❌ Writing complete implementation, then adding tests
- ❌ Writing multiple tests before any implementation
- ❌ Skipping the "run test and watch it fail" step
When This Skill Applies
This skill is MANDATORY for:
- ✅ Implementing new features
- ✅ Creating new functions or modules
- ✅ Writing deciders or projections (event sourcing)
- ✅ Creating HTTP handlers
- ✅ Fixing bugs (write test that reproduces bug first)
- ✅ Refactoring (tests ensure behavior doesn't change)
- ✅ Adding business logic
This skill does NOT apply to:
- ❌ Configuration files (package.json, tsconfig.json)
- ❌ View templates (.ejs files)
- ❌ Documentation files (.md)
- ❌ Build scripts
Project-Specific Test Patterns
Test File Naming Convention
Implementation: src/usecases/budget/allocate.ts
Test: src/usecases/budget/allocate.spec.ts
Implementation: src/products/add.ts
Test: src/products/add.spec.ts
Pattern: Same directory, same name, .spec.ts extension
Running Tests
# Run all tests in a package
bun test
# Run specific test file
node --import tsx --test 'src/usecases/budget/allocate.spec.ts'
# Run tests matching pattern
node --import tsx --test 'src/**/*.spec.ts'
Event Sourcing Decider Tests
For deciders, test both decide() and evolve() functions:
import { describe, it } from "node:test"
import assert from "node:assert"
import { AllocateBudget, type AllocateBudgetCommand } from "./allocate"
describe("AllocateBudget", () => {
describe("decide", () => {
it("should create budget.allocated event when budget does not exist", async () => {
const decider = AllocateBudget()
const command: AllocateBudgetCommand = {
type: "budget.allocate",
id: "budget_123",
description: "Groceries",
amount: 500,
frequency: "monthly",
}
const events = await decider.decide(command, decider.initialState())
assert.strictEqual(events.length, 1)
assert.strictEqual(events[0].event.type, "budget.allocated")
assert.strictEqual(events[0].event.id, "budget_123")
assert.strictEqual(events[0].event.description, "Groceries")
assert.strictEqual(events[0].event.amount, 500)
assert.strictEqual(events[0].event.frequency, "monthly")
})
it("should return empty array when budget already exists (idempotency)", async () => {
const decider = AllocateBudget()
const command: AllocateBudgetCommand = {
type: "budget.allocate",
id: "budget_123",
description: "Groceries",
amount: 500,
frequency: "monthly",
}
// First allocation
const events = await decider.decide(command, decider.initialState())
// Evolve state
const state = decider.evolve(decider.initialState(), {
position: 1n,
timestamp: new Date(),
streamId: ["budget_123"],
type: "budget.allocated",
event: events[0].event,
})
// Second allocation (should be idempotent)
const secondEvents = await decider.decide(command, state)
assert.strictEqual(secondEvents.length, 0)
})
})
describe("evolve", () => {
it("should mark budget as allocated in state", () => {
const decider = AllocateBudget()
const initialState = decider.initialState()
const state = decider.evolve(initialState, {
position: 1n,
timestamp: new Date(),
streamId: ["budget_123"],
type: "budget.allocated",
event: {
type: "budget.allocated",
id: "budget_123",
description: "Groceries",
amount: 500,
frequency: "monthly",
},
})
assert.strictEqual(state["budget_123"], true)
})
})
})
Projection Tests
For projections, test the catchup() method and query methods:
import { describe, it } from "node:test"
import assert from "node:assert"
import { eventstore } from "@groceries/event-sourcing"
import { InMemoryProjection } from "./in-memory-projection"
import type { BudgetEvent } from "./event"
describe("BudgetProjection", () => {
it("should return empty array when no budgets exist", async () => {
const store = new eventstore.InMemory<BudgetEvent>()
const projection = InMemoryProjection(store)
await projection.catchup()
const budgets = await projection.all()
assert.strictEqual(budgets.length, 0)
})
it("should return allocated budget after catchup", async () => {
const store = new eventstore.InMemory<BudgetEvent>()
await store.save([{
streamId: ["budget_123"],
type: "budget.allocated",
event: {
type: "budget.allocated",
id: "budget_123",
description: "Groceries",
amount: 500,
frequency: "monthly",
},
}])
const projection = InMemoryProjection(store)
await projection.catchup()
const budgets = await projection.all()
assert.strictEqual(budgets.length, 1)
assert.strictEqual(budgets[0].id, "budget_123")
assert.strictEqual(budgets[0].description, "Groceries")
assert.strictEqual(budgets[0].amount, 500)
assert.strictEqual(budgets[0].frequency, "monthly")
})
it("should return budget by id", async () => {
const store = new eventstore.InMemory<BudgetEvent>()
await store.save([{
streamId: ["budget_123"],
type: "budget.allocated",
event: {
type: "budget.allocated",
id: "budget_123",
description: "Groceries",
amount: 500,
frequency: "monthly",
},
}])
const projection = InMemoryProjection(store)
await projection.catchup()
const budget = await projection.byId("budget_123")
assert.strictEqual(budget.id, "budget_123")
assert.strictEqual(budget.description, "Groceries")
})
it("should throw error when budget not found", async () => {
const store = new eventstore.InMemory<BudgetEvent>()
const projection = InMemoryProjection(store)
await projection.catchup()
await assert.rejects(
async () => await projection.byId("budget_nonexistent"),
{ name: "BudgetNotFoundError" }
)
})
})
TDD Workflow (Kent Beck Style)
CRITICAL: Work on ONE test at a time. NEVER write multiple tests before implementation.
The Micro-Cycle (Repeat This For Each Test)
Step 1: Write ONE Test (Just One!)
Write a SINGLE test case. Not multiple tests. Just one.
// CORRECT: Write ONE test
it("should create budget.allocated event when budget does not exist", async () => {
// test code here
})
// WRONG: Don't write multiple tests at once
it("should create event...", async () => { })
it("should be idempotent...", async () => { }) // ❌ STOP! One at a time!
it("should reject negative...", async () => { }) // ❌ NO!
Step 2: Run the Test (Watch It Fail - RED)
Run ONLY this test. It MUST fail. This proves the test is actually testing something.
node --import tsx --test 'src/usecases/budget/allocate.spec.ts'
Expected output: ❌ Test fails (RED)
If it passes without implementation, the test is wrong or testing the wrong thing.
Step 3: Write Minimal Implementation (Make It Pass - GREEN)
Write the SMALLEST amount of code to make THIS ONE test pass. Don't think about future tests.
node --import tsx --test 'src/usecases/budget/allocate.spec.ts'
Expected output: ✅ Test passes (GREEN)
Step 4: Refactor (Keep It GREEN)
Only now, improve the code:
- Remove duplication
- Improve names
- Simplify logic
After EACH change, run tests to ensure they still pass.
Step 5: Repeat the Cycle
Now go back to Step 1 and write THE NEXT test. One test at a time.
Planning Tests (Mental Note Only)
You may mentally list what needs testing:
Mental checklist (don't write these tests yet):
- create event when doesn't exist ← Start here (write this test first)
- idempotent behavior ← Write this test AFTER first passes
- reject negative amounts ← Write this test AFTER second passes
- reject invalid frequency ← Write this test AFTER third passes
But write code for only ONE at a time.
Anti-Patterns (What NOT to Do)
❌ WRONG: Writing Implementation First
// DON'T DO THIS - No test written first!
export function AllocateBudget(): Decider<...> {
return {
decide: async (command, state) => {
// Implementation without tests
}
}
}
✅ RIGHT: Writing Test First
// Step 1: Write the test FIRST
describe("AllocateBudget", () => {
it("should create budget.allocated event", async () => {
const decider = AllocateBudget()
const events = await decider.decide(command, state)
assert.strictEqual(events.length, 1)
})
})
// Step 2: Run test (it fails - good!)
// Step 3: Write minimal implementation to pass
❌ WRONG: Skipping Test Verification
Don't assume tests pass. Always run them:
# ALWAYS run tests to verify
bun test
❌ WRONG: Writing All Tests First, Then Implementation
❌ write allocate.spec.ts (ALL tests at once)
- test 1 ❌
- test 2 ❌
- test 3 ❌
- test 4 ❌
❌ write allocate.ts (complete implementation)
- all tests pass ✅
Why this is wrong: You lose the benefit of incremental design. Tests should guide your implementation one step at a time.
❌ WRONG: Writing All Code, Then All Tests
❌ write allocate.ts (complete implementation)
❌ write projection.ts (complete implementation)
❌ write allocate.spec.ts (all tests)
❌ write projection.spec.ts (all tests)
Why this is wrong: Tests written after implementation are just checking existing behavior, not driving design.
✅ RIGHT: Micro-Cycle - ONE Test, Minimal Code, Repeat
Cycle 1:
✅ write allocate.spec.ts (ONLY test 1: "should create event")
✅ run test → ❌ FAILS (good!)
✅ write allocate.ts (minimal code to pass test 1)
✅ run test → ✅ PASSES
✅ refactor if needed
✅ run test → ✅ still passes
Cycle 2:
✅ write allocate.spec.ts (add ONLY test 2: "should be idempotent")
✅ run test → ❌ FAILS (good!)
✅ update allocate.ts (minimal code to pass test 2)
✅ run test → ✅ PASSES
✅ refactor if needed
✅ run test → ✅ still passes
Cycle 3:
✅ write allocate.spec.ts (add ONLY test 3: "should reject negative amounts")
✅ run test → ❌ FAILS (good!)
✅ update allocate.ts (minimal code to pass test 3)
✅ run test → ✅ PASSES
... repeat for each remaining test
Why this is right: Each test drives exactly the code needed. Implementation emerges from requirements.
Checklist Before Completing Any Task
Before marking a coding task as complete, verify you followed the micro-cycle for EACH test:
For EACH test case, did you:
- Write exactly ONE test (not multiple)
- Run the test and verify it FAILED (Red)
- Write MINIMAL implementation to pass THAT test
- Run the test and verify it PASSED (Green)
- Refactor (if needed)
- Verify tests still pass after refactoring
- Then move to NEXT test (repeat above)
Overall checklist:
- Test file created with
.spec.tsextension - Followed Red-Green-Refactor for every single test
- No existing tests broken
- Edge cases covered (empty arrays, null values, errors)
- Idempotency tested (for deciders)
- All tests run and pass
Red flags (indicates you did NOT follow TDD):
- 🚩 You wrote more than one test before implementation
- 🚩 You wrote implementation before any tests
- 🚩 You didn't run tests to see them fail
- 🚩 You wrote "complete" implementation in one go
Test Assertion Library
Use Node.js built-in assert module:
import assert from "node:assert"
// Equality checks
assert.strictEqual(actual, expected)
assert.deepStrictEqual(actualObject, expectedObject)
assert.notStrictEqual(actual, notExpected)
// Boolean checks
assert.ok(value)
assert.strictEqual(value, true)
// Array/object checks
assert.strictEqual(array.length, 3)
assert.deepStrictEqual(obj, { id: "123", name: "Alice" })
// Error checks
await assert.rejects(
async () => await functionThatThrows(),
{ name: "ExpectedErrorName" }
)
assert.throws(
() => functionThatThrows(),
/expected error message/
)
Test Organization Structure
import { describe, it } from "node:test"
import assert from "node:assert"
describe("Feature/Module Name", () => {
describe("specific function or method", () => {
it("should [expected behavior] when [condition]", () => {
// Arrange: Set up test data
const input = { ... }
// Act: Call the function
const result = functionUnderTest(input)
// Assert: Verify the result
assert.strictEqual(result, expectedValue)
})
})
})
Examples from Codebase
Reference existing test files as examples:
apps/app/src/usecases/product/add.spec.ts- Decider testsapps/app/src/usecases/product/in-memory-projection.spec.ts- Projection testsapps/app/src/usecases/product/change-name.spec.ts- Event evolution tests
Summary: The TDD Mantra
ONE Test → Red → Green → Refactor → REPEAT
- ONE Test: Write exactly ONE test case
- Red: Run it, watch it fail
- Green: Write minimal code to make THAT test pass
- Refactor: Clean up while keeping tests green
- REPEAT: Go back to step 1 for the NEXT test
The Golden Rules:
- Write ONE test at a time (never batch tests)
- Always watch tests fail before writing implementation
- Write the simplest code that passes the current test
- Refactor only after tests pass
- Tests are specifications, not documentation
What TDD is NOT:
- ❌ Writing all tests, then all implementation
- ❌ Writing implementation, then retrofitting tests
- ❌ Writing multiple tests before any implementation
What TDD IS:
- ✅ Tiny cycles: one test → code → refactor → next test
- ✅ Tests drive design incrementally
- ✅ Each test adds ONE new behavior