Test-Driven Development (TDD) Workflow
This skill enforces Critical Invariant #7: Always write tests BEFORE implementation.
When to Use This Skill
ALWAYS use for:
- •New features or functionality
- •New API endpoints or models
- •New UI components with logic
- •Bug fixes (write test that reproduces bug first)
Trigger words:
- •"implement", "add feature", "create new"
- •"TDD", "test first", "test-driven"
- •"red-green-refactor"
The TDD Cycle: RED-GREEN-REFACTOR
🔴 Phase 1: RED (Write Failing Test)
Step 1: Create test file
# If test file doesn't exist touch test/my-feature.test.js
Step 2: Write the test FIRST
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { myFeature } from '../lib/my-feature.js'
describe('myFeature', () => {
it('should return expected result when given valid input', () => {
const result = myFeature({ input: 'test' })
assert.strictEqual(result.success, true)
assert.strictEqual(result.output, 'processed: test')
})
})
Step 3: Run test - MUST FAIL
npm test # Expected output: # ✗ myFeature > should return expected result... # Error: Cannot find module '../lib/my-feature.js'
✅ Success criteria:
- •Test fails for the RIGHT reason
- •Error message is clear
- •You understand what needs to be implemented
❌ If test passes already:
- •You're not writing a new test
- •Test doesn't actually test the feature
- •Implementation already exists (not TDD)
🟢 Phase 2: GREEN (Make Test Pass)
Step 4: Implement MINIMAL code
Write the SIMPLEST code that makes the test pass:
// lib/my-feature.js
export function myFeature({ input }) {
// Minimal implementation - just make the test pass
return {
success: true,
output: `processed: ${input}`
}
}
Don't add:
- •Extra features not tested
- •Edge case handling not in test
- •Optimizations or clever code
- •Multiple responsibilities
Step 5: Run test - MUST PASS
npm test # Expected output: # ✓ myFeature > should return expected result... # Tests: 1 passed, 1 total
✅ Success criteria:
- •All tests pass (including existing ones)
- •Implementation is simple and clear
- •Code does ONLY what tests require
🔵 Phase 3: REFACTOR (Improve While Green)
Step 6: Make it better
Now that tests pass, improve the code:
// lib/my-feature.js
const PREFIX = 'processed: '
export function myFeature({ input }) {
// Refactored: extracted constant, added validation
if (!input || typeof input !== 'string') {
throw new Error('Input must be a non-empty string')
}
return {
success: true,
output: `${PREFIX}${input.trim()}`
}
}
Step 7: Add tests for edge cases
describe('myFeature', () => {
it('should return expected result when given valid input', () => {
// ... existing test
})
it('should throw error for invalid input', () => {
assert.throws(
() => myFeature({ input: '' }),
{ message: 'Input must be a non-empty string' }
)
})
it('should trim whitespace from input', () => {
const result = myFeature({ input: ' test ' })
assert.strictEqual(result.output, 'processed: test')
})
})
Step 8: Run tests - MUST STILL PASS
npm test # Expected output: # ✓ myFeature > should return expected result... # ✓ myFeature > should throw error for invalid input # ✓ myFeature > should trim whitespace from input # Tests: 3 passed, 3 total
✅ Success criteria:
- •Code is cleaner/more maintainable
- •All tests still pass
- •No new functionality without tests
Full Workflow Example
Scenario: Add a user validation function
1. 🔴 RED: Write failing test
// test/validate-user.test.js
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { validateUser } from '../lib/validate-user.js'
describe('validateUser', () => {
it('should return valid:true for user with required fields', () => {
const user = { name: 'Alice', email: 'alice@example.com' }
const result = validateUser(user)
assert.strictEqual(result.valid, true)
assert.strictEqual(result.errors, undefined)
})
})
Run: npm test → ❌ Fails (module doesn't exist)
2. 🟢 GREEN: Minimal implementation
// lib/validate-user.js
export function validateUser(user) {
return { valid: true }
}
Run: npm test → ✅ Passes
3. 🔵 REFACTOR: Add more tests + improve
// test/validate-user.test.js (add more tests)
describe('validateUser', () => {
it('should return valid:true for user with required fields', () => {
// ... existing test
})
it('should return valid:false when name is missing', () => {
const user = { email: 'alice@example.com' }
const result = validateUser(user)
assert.strictEqual(result.valid, false)
assert.deepStrictEqual(result.errors, ['Name is required'])
})
it('should return valid:false when email is invalid', () => {
const user = { name: 'Alice', email: 'not-an-email' }
const result = validateUser(user)
assert.strictEqual(result.valid, false)
assert.ok(result.errors.some(e => e.includes('email')))
})
})
Run: npm test → ❌ Fails (need to implement validation)
// lib/validate-user.js (improved implementation)
export function validateUser(user) {
const errors = []
if (!user.name || user.name.trim() === '') {
errors.push('Name is required')
}
if (!user.email || !isValidEmail(user.email)) {
errors.push('Valid email is required')
}
return {
valid: errors.length === 0,
errors: errors.length > 0 ? errors : undefined
}
}
function isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}
Run: npm test → ✅ Passes
Common Pitfalls
❌ Writing tests after implementation
Problem: Tests become "verification tests" not "design tests" Solution: Delete implementation, start over with test first
❌ Writing too many tests at once
Problem: Overwhelmed, unclear what to implement Solution: One test at a time, make it pass, then next test
❌ Implementing more than needed to pass test
Problem: Scope creep, untested code paths Solution: Implement ONLY what makes current test pass
❌ Skipping RED phase
Problem: Test might always pass (false positive) Solution: ALWAYS watch test fail before implementing
❌ Not refactoring
Problem: Code works but is messy Solution: After GREEN, improve code while tests stay green
TDD Benefits
Design Benefits
- •Forces thinking about API before implementation
- •Creates minimal, focused interfaces
- •Reveals coupling and dependencies early
Confidence Benefits
- •Every line of code has a test
- •Refactoring is safe (tests catch regressions)
- •Documentation through examples
Workflow Benefits
- •Clear progress (count passing tests)
- •Natural stopping points
- •Easy to resume work (look at failing test)
Integration with Project Workflow
Before Starting Feature
# 1. Read skill cat .github/skills/tdd-workflow/SKILL.md # 2. Create test file touch test/my-feature.test.js # 3. Start TDD cycle (RED)
During Implementation
# Continuous cycle: # 🔴 Write failing test # 🟢 Make it pass # 🔵 Refactor # Repeat
Before Committing
# Run all tests npm test # Git hook will check for test changes git add lib/my-feature.js test/my-feature.test.js git commit -m "feat: add my-feature (TDD)"
Self-Audit (from AGENTS.md Step 3½)
- • Did I write test BEFORE implementation? (RED)
- • Did I see test fail first?
- • Did I implement minimal code to pass? (GREEN)
- • Did I refactor while keeping tests green? (REFACTOR)
Enforcement Mechanisms
This project has 4 layers of TDD enforcement:
- •AGENTS.md - Pre-work checklist (Step 3)
- •feature-implementation skill - Phase 0 TDD requirement
- •Git hook -
.git-hooks/pre-commitchecks for test changes - •GitHub Actions -
.github/workflows/tdd-compliance.ymlPR check
To install git hook:
./scripts/install-git-hooks.sh
Quick Reference
RED-GREEN-REFACTOR Cycle:
- •🔴 Write failing test
- •Run test (must fail)
- •🟢 Write minimal code
- •Run test (must pass)
- •🔵 Refactor code
- •Run test (must still pass)
- •Repeat
Golden Rules:
- •Test FIRST, code second
- •One test at a time
- •Simplest code to pass
- •All tests must pass before commit
- •Refactor only when green
This skill enforces Critical Invariant #7 from CODEBASE_ESSENTIALS.md