| name | tdd |
| description | Enforce test-driven development with RED→GREEN→REFACTOR cycle and coverage validation |
| allowed-tools | Read, Write, Edit, Bash, Glob, Grep |
/tdd - Test-Driven Development Workflow
Develop a feature using strict test-driven development: write failing tests first, implement minimal code, refactor, and verify coverage.
What a good test is: tests verify behavior through public interfaces, read like a specification, and survive refactors.
Task description: $ARGUMENTS
TDD Cycle (MANDATORY)
RED → GREEN → REFACTOR → REPEAT
- RED: Write a failing test FIRST (verify test fails before implementation)
- GREEN: Write minimal code to pass the test (no over-engineering)
- REFACTOR: Improve code quality while keeping tests green
- REPEAT: Continue until feature is complete and coverage meets requirements
Seams — where tests go
A seam is the public boundary you test at: the interface where you observe behavior without reaching inside. Tests live at seams, never against internals.
Test only at pre-agreed seams. Before writing any test, write down the seams under test and confirm them with the user. No test is written at an unconfirmed seam. You can't test everything — agreeing the seams up front is how testing effort lands on the critical paths and complex logic instead of every edge case.
Ask: "What's the public interface, and which seams should we test?"
Process
Step 1: Framework Detection & Setup
Auto-detect the test framework and package manager from the project:
Package managers (check in priority order):
bun.lockb → bun test
pnpm-lock.yaml → pnpm test
yarn.lock → yarn test
package-lock.json → npm test
Cargo.lock → cargo test
go.mod → go test ./...
Test frameworks (infer from project files):
package.json with jest → Jest (Node/TypeScript)
package.json with vitest → Vitest (Vite-based)
pyproject.toml or requirements.txt → pytest (Python)
go.mod → Go's testing package
Cargo.toml → Rust's built-in test framework
If no framework detected: Ask the user which test framework and runner to use. Do not proceed without a confirmed test runner.
Store the detected runner as {TEST_CMD} (e.g., bun test, pnpm test, pytest) and use it for all subsequent test commands in this workflow.
Report detected framework and package manager to the user before proceeding.
Step 2: Define Interfaces (Scaffold)
Create the function/class definition with empty implementation:
- Write TypeScript interfaces (or equivalent) for inputs and outputs
- Define the function signature with placeholder implementation
- Include JSDoc comments describing behavior
Example (TypeScript):
export function validateEmail(email: string): boolean {
throw new Error('Not implemented');
}
Step 3: Write Failing Tests (RED Phase)
Read <repo>/CONTEXT.md if present so test names and interface vocabulary match the project's domain language (ties into /domain-modeling).
Write comprehensive test suite BEFORE implementation:
Test types to include:
- Happy path (valid input → expected output)
- Edge cases (empty, null, undefined, boundary values, max length)
- Error conditions (invalid formats, type errors)
- Integration tests (API endpoints, database operations if applicable)
Requirements:
- Tests must FAIL before implementation
- Each test should be independent (no shared state)
- Use meaningful assertion messages
- Run tests and VERIFY RED status (all fail as expected)
Example test structure (Jest/Vitest):
describe('validateEmail', () => {
describe('happy path', () => {
it('accepts valid email addresses', () => {
expect(validateEmail('user@example.com')).toBe(true);
expect(validateEmail('test.name+tag@domain.co.uk')).toBe(true);
});
});
describe('edge cases', () => {
it('rejects empty string', () => {
expect(validateEmail('')).toBe(false);
});
it('rejects email without @', () => {
expect(validateEmail('invalidemail.com')).toBe(false);
});
it('rejects email without domain', () => {
expect(validateEmail('user@')).toBe(false);
});
it('rejects email with spaces', () => {
expect(validateEmail('user @example.com')).toBe(false);
});
it('handles special characters', () => {
expect(validateEmail('user+tag@example.com')).toBe(true);
expect(validateEmail('user.name@sub.domain.com')).toBe(true);
});
});
describe('error conditions', () => {
it('throws error if email is null', () => {
expect(() => validateEmail(null as any)).toThrow();
});
it('throws error if email is undefined', () => {
expect(() => validateEmail(undefined as any)).toThrow();
});
it('rejects excessively long email', () => {
const longEmail = 'a'.repeat(255) + '@example.com';
expect(validateEmail(longEmail)).toBe(false);
});
});
});
Run tests and verify RED:
{TEST_CMD}
Step 4: Implement Minimal Code (GREEN Phase)
Write only the minimum code needed to pass the tests:
- No premature optimization
- No extra features beyond test requirements
- Focus on making tests pass
- Keep code simple and readable
Example minimal implementation:
export function validateEmail(email: string): boolean {
if (email === null || email === undefined) {
throw new Error('Email cannot be null or undefined');
}
if (email.length === 0 || email.length > 254) {
return false;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
Run tests and verify GREEN:
{TEST_CMD}
Step 5: Refactor (IMPROVE Phase)
Improve code quality while keeping tests green:
- Extract constants and magic strings
- Improve variable/function names
- Remove duplication
- Optimize performance (if needed)
- Add comments for complex logic
Requirements:
- Run tests after each refactoring step
- Verify all tests still PASS
- Refactoring should not add new functionality
- Keep in-loop refactors small and local. For deeper structural refactors (moving seams, reshaping modules), hand off to
/review rather than expanding this cycle.
Example refactored implementation:
const EMAIL_MAX_LENGTH = 254;
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export function validateEmail(email: string): boolean {
if (email == null) {
throw new Error('Email cannot be null or undefined');
}
if (email.length === 0 || email.length > EMAIL_MAX_LENGTH) {
return false;
}
return EMAIL_PATTERN.test(email);
}
Run tests after EACH refactoring step:
{TEST_CMD}
Step 6: Verify Coverage
Check code coverage and add tests for any uncovered paths:
Coverage requirements by code type:
| Code Type | Minimum Coverage |
|---|
| Standard application code | 80% |
| Financial calculations | 100% |
| Authentication/authorization | 100% |
| Security-critical code | 100% |
| Encryption/decryption | 100% |
Run coverage check:
npm test -- --coverage
npx vitest run --coverage
pytest --cov=. --cov-report=term-missing
go test -cover ./...
cargo tarpaulin --out Html
Acceptance criteria:
- Lines covered: ≥ target percentage
- Branches covered: ≥ target percentage
- Functions covered: ≥ target percentage
- Any uncovered lines documented with rationale (if intentional)
If coverage is below target:
- Identify uncovered code paths
- Write additional tests for those paths
- Run tests again (verify GREEN)
- Re-check coverage
Worked Example: Email Validator (Complete Walkthrough)
RED Phase Output
$ npm test
FAIL src/email-validator.test.ts
validateEmail
happy path
✕ accepts valid email addresses (0ms)
✕ accepts variation formats (1ms)
edge cases
✕ rejects empty string (0ms)
✕ rejects email without @ (1ms)
✕ rejects email without domain (0ms)
✕ rejects email with spaces (0ms)
✕ handles special characters (1ms)
error conditions
✕ throws error if email is null (0ms)
✕ throws error if email is undefined (0ms)
✕ rejects excessively long email (0ms)
Tests: 0 passed, 10 failed, 10 total
GREEN Phase Output (After Implementation)
$ npm test
PASS src/email-validator.test.ts
validateEmail
happy path
✓ accepts valid email addresses (2ms)
✓ accepts variation formats (1ms)
edge cases
✓ rejects empty string (0ms)
✓ rejects email without @ (0ms)
✓ rejects email without domain (1ms)
✓ rejects email with spaces (0ms)
✓ handles special characters (1ms)
error conditions
✓ throws error if email is null (0ms)
✓ throws error if email is undefined (0ms)
✓ rejects excessively long email (0ms)
Tests: 10 passed, 10 total (18ms)
Coverage Report (After Implementation & Refactoring)
$ npm test -- --coverage
------------|----------|----------|----------|----------|-------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered |
------------|----------|----------|----------|----------|-------------|
All files | 100 | 100 | 100 | 100 | |
email-...ts | 100 | 100 | 100 | 100 | |
------------|----------|----------|----------|----------|-------------|
Anti-Patterns to Avoid
- Testing implementation details: Test behavior, not internal state
- Shared test state: Each test must be independent (no
beforeAll side effects)
- Insufficient assertions: Verify specific outputs, not just "no error"
- Untested error paths: Include tests for error conditions, not just happy path
- No mocking of external dependencies: Mock API calls, database, file system, etc.
- Skipped tests: Never commit
xit() or .skip — fix the test or remove it
- Tautological: The assertion recomputes the expected value the way the code does (
expect(add(a, b)).toBe(a + b), or a snapshot derived by hand the same way) — it passes by construction and can never disagree with the code. Expected values must come from an independent source of truth (a known-good literal, a worked example, the spec).
- Horizontal slicing: Writing all tests first, then all implementation. Bulk tests verify imagined behavior and commit you to test structure before you understand the implementation. Work in vertical slices instead — one test → one implementation → repeat, each test a tracer bullet that responds to what the last cycle taught you.
Tips for Success
- Write tests first, always — This forces you to think about the interface before implementation
- Fail fast — The RED phase should show clear test failures before coding
- Minimal code — The simplest code that passes tests is usually the best code
- Refactor fearlessly — Tests are your safety net; they prevent regressions
- Coverage is a tool, not a goal — 80% coverage is a checkpoint, not a target
- Test the interface, not the implementation — Users care about behavior, not how you achieve it
Framework-Specific Commands
Jest (Node.js/TypeScript)
npm test
npm test -- --watch
npm test -- --coverage
npm test email-validator.test.ts
Vitest (Vite/TypeScript)
npx vitest run
npx vitest
npx vitest run --coverage
npx vitest email-validator.test.ts
pytest (Python)
pytest
pytest --lf (runs last-failed)
pytest --cov=. --cov-report=term-missing
pytest tests/test_email_validator.py
go test (Go)
go test ./...
go test -v ./...
go test -cover ./...
go test -run TestValidateEmail ./...
cargo test (Rust)
cargo test
cargo test --release
cargo tarpaulin --out Html
cargo test validate_email
Success Checklist
After completing the TDD workflow, verify: