TDD
Purpose
Enforce Test-Driven Development discipline. All changes must have tests written first, tests must fail before implementation, and when tests fail, fix the code not the tests.
Quick Reference
- Setup:
/tdd configure(run once during framework setup) - Usage:
/tdd(uses saved config) - Update:
/tdd learn <path>(analyze specific path) - Config:
.claude/skills/tdd.yaml
Commands
| Command | Purpose | When to Use |
|---|---|---|
/tdd configure | Analyze entire project for test patterns | Framework setup / wizard |
/tdd learn <path> | Analyze specific path, update config | New module added |
/tdd | Write tests using saved patterns | Normal development |
/tdd configure
When: Framework setup wizard (one-time)
What it does:
- Scans entire project for testing patterns
- Proposes findings to user
- User approves/modifies
- Saves to
.claude/skills/tdd.yaml
Discovery Process
1. DETECT TEST FRAMEWORK
├─ JavaScript/TypeScript: package.json → jest, vitest, mocha, ava
├─ Python: pyproject.toml, requirements.txt → pytest, unittest
├─ Go: *_test.go files → go test
└─ Rust: Cargo.toml → cargo test
2. FIND TEST LOCATIONS
├─ Co-located: src/**/*.test.ts
├─ Separate: tests/, __tests__/, test/
└─ Pattern: *.test.*, *_test.*, test_*
3. ANALYZE TEST STRUCTURE (read 5-10 test files)
├─ Organization: describe/it, class-based, function-based
├─ Assertion style: expect, assert, require
├─ Mock patterns: vi.fn(), jest.mock(), @patch, etc.
└─ Fixture patterns: beforeEach, fixtures/, conftest.py
4. DETECT TEST COMMANDS
├─ Taskfile.yaml: task test, task test:unit
├─ package.json: npm test, npm run test:*
├─ Makefile: make test
└─ Direct: pytest, go test, cargo test
Proposal Format
# Proposed TDD Configuration
# Review and approve to save to .claude/skills/tdd.yaml
framework: vitest
language: typescript
test_locations:
pattern: "**/*.test.ts"
style: co-located # or: separate
structure:
organization: describe-it
example: |
describe('ModuleName', () => {
describe('methodName', () => {
it('should behavior when condition', () => {
// arrange, act, assert
});
});
});
assertions:
library: vitest
style: expect
example: "expect(result).toBe(expected)"
mocks:
library: vitest
pattern: vi.fn()
example: |
const mockDep = { method: vi.fn() };
mockDep.method.mockResolvedValue(data);
fixtures:
setup: beforeEach
teardown: afterEach
data_location: null # or: __fixtures__/, tests/fixtures/
commands:
all: "task test"
unit: "task test"
watch: "task test -- --watch"
single: "task test -- {file}"
Save Location
Config path depends on how the plugin was installed:
| Plugin Scope | Config File | Git |
|---|---|---|
| project | .claude/skills/tdd.yaml | Committed (shared) |
| local | .claude/skills/tdd.local.yaml | Ignored (personal) |
| user | .claude/skills/tdd.local.yaml | Ignored (personal) |
Precedence when reading (first found wins):
.claude/skills/tdd.local.yaml.claude/skills/tdd.yaml- Skill defaults
/tdd learn
When: New module added, want to capture its patterns
What it does:
- Analyzes test files in specified path
- Compares to existing config
- Proposes updates if new patterns found
- Updates
.claude/skills/tdd.yaml
Example
/tdd learn src/new-service/
Analyzing tests in src/new-service/...
Found 3 test files:
- src/new-service/handler.test.ts
- src/new-service/service.test.ts
- src/new-service/utils.test.ts
New patterns detected:
- Mock pattern: vi.spyOn() (not in current config)
- Fixture: uses beforeAll for expensive setup
Propose adding to config:
mocks.additional_patterns:
- vi.spyOn(object, 'method')
fixtures.expensive_setup: beforeAll
[Approve / Modify / Skip]
/tdd (Normal Usage)
When: Writing tests during development
Requires: .claude/skills/tdd.yaml exists (run /tdd configure first)
TDD Cycle
┌─────────────────────────────────────────────────────────────┐
│ TDD CYCLE │
├─────────────────────────────────────────────────────────────┤
│ 1. RED → Write failing test (using saved patterns) │
│ 2. GREEN → Write minimal code to pass │
│ 3. REFACTOR → Clean up with tests green │
└─────────────────────────────────────────────────────────────┘
Procedure
- Read saved config from
.claude/skills/tdd.yaml - Define expected behavior (Flow Spec)
- Write test first using project's patterns (must fail - RED)
- Write minimal code to pass (GREEN)
- Refactor with tests green
- Verify:
task test
Flow Spec Template
## Flow Spec: {feature_name}
- **Name**: {descriptive name}
- **Entry point(s)**: (Function, method, endpoint)
- **Inputs**: (Parameters, dependencies)
- **Expected outputs**: (Return values, side effects)
- **Invariants**: (What must always be true)
- **Edge cases**: (Boundaries, error conditions)
Non-Negotiables
- Tests first: Tests must fail before code exists
- Fix code, not tests: If a test fails, fix the implementation
- No behavior without tests: Every new function needs a test
- Match project patterns: Use patterns from saved config
Definition of Done
-
.claude/skills/tdd.yamlexists (discovery completed) - Tests written first (failed before implementation)
- Tests match project patterns from config
-
task testpasses (all tests green) -
task precommitpasses
Anti-Patterns
| Don't | Do Instead |
|---|---|
| Write code first, tests later | Write failing test first |
| Modify test to pass | Fix the implementation |
| Invent new test patterns | Use patterns from saved config |
| Re-configure every time | Read saved .claude/skills/tdd.yaml |
| Skip discovery | Run /tdd configure during setup |
Config Schema
# .claude/skills/tdd.yaml
version: 1
discovered_at: "2024-01-26T12:00:00Z"
framework: vitest | jest | pytest | go | cargo
language: typescript | python | go | rust
test_locations:
pattern: "**/*.test.ts"
style: co-located | separate
directories: [] # if separate style
structure:
organization: describe-it | class-based | function-based
naming: "should {behavior} when {condition}"
example: |
# actual example from project
assertions:
library: vitest | jest | pytest | testify
style: expect | assert
example: "expect(x).toBe(y)"
mocks:
library: vitest | jest | unittest.mock | gomock
pattern: "vi.fn()"
example: |
# actual example from project
fixtures:
setup: beforeEach | setUp | func setup
teardown: afterEach | tearDown | func teardown
data_location: null | path
commands:
all: "task test"
unit: "task test"
watch: "task test -- --watch"
single: "task test -- {file}"
