| name | tdd-workflow |
| description | Complete Test-Driven Development workflow with red-green-refactor cycle |
| license | MIT |
TDD Workflow Skill
Use this skill when implementing features using Test-Driven Development methodology.
When to Use
- Implementing new features or functions
- Adding new API endpoints
- Creating utility functions
- Building components with complex logic
- When high test coverage is required
TDD Cycle
┌──────────────────────────────────────────────────────────┐
│ │
│ 🔴 RED → 🟢 GREEN → 🔵 REFACTOR │
│ Write test Make pass Improve code │
│ that fails (minimal) (keep green) │
│ │
│ ↺ Repeat │
└──────────────────────────────────────────────────────────┘
Step-by-Step Process
Phase 1: RED - Write Failing Test
-
Understand the requirement
- What is the input?
- What is the expected output?
- What are the edge cases?
-
Write the simplest test case first
describe('calculateDiscount', () => {
it('should return 0 for orders under $50', () => {
expect(calculateDiscount(30)).toBe(0);
});
});
-
Run the test - it MUST fail
npm test -- calculateDiscount
Phase 2: GREEN - Make It Pass
-
Write the minimal code to pass
function calculateDiscount(orderTotal: number): number {
return 0;
}
-
Run the test - it MUST pass
npm test -- calculateDiscount
-
Don't over-engineer - Only write enough code to pass the current test
Phase 3: REFACTOR - Improve the Code
- Only refactor when tests are green
- Keep tests passing throughout
- Improve code quality:
- Extract constants
- Rename for clarity
- Remove duplication
- Simplify logic
Phase 4: Repeat
Add the next test case and repeat the cycle:
it('should return 10% for orders $50-$100', () => {
expect(calculateDiscount(75)).toBe(7.5);
});
Test Structure
Arrange-Act-Assert Pattern
it('should apply discount to premium users', () => {
const user = createUser({ isPremium: true });
const order = createOrder({ total: 100 });
const result = applyDiscount(user, order);
expect(result.total).toBe(90);
expect(result.discountApplied).toBe(10);
});
Test Naming Convention
it('should return empty array when no items match filter', () => {});
it('should throw ValidationError when email is invalid', () => {});
it('should retry 3 times when API returns 503', () => {});
Complete Example
Requirement
Create a password validator that checks:
- Minimum 8 characters
- At least one uppercase letter
- At least one number
TDD Implementation
describe('validatePassword', () => {
it('should reject passwords shorter than 8 characters', () => {
expect(validatePassword('Short1')).toEqual({
valid: false,
errors: ['Password must be at least 8 characters'],
});
});
});
function validatePassword(password: string) {
const errors: string[] = [];
if (password.length < 8) {
errors.push('Password must be at least 8 characters');
}
return { valid: errors.length === 0, errors };
}
it('should reject passwords without uppercase', () => {
expect(validatePassword('lowercase1')).toEqual({
valid: false,
errors: ['Password must contain an uppercase letter'],
});
});
function validatePassword(password: string) {
const errors: string[] = [];
if (password.length < 8) {
errors.push('Password must be at least 8 characters');
}
if (!/[A-Z]/.test(password)) {
errors.push('Password must contain an uppercase letter');
}
return { valid: errors.length === 0, errors };
}
it('should reject passwords without numbers', () => {
expect(validatePassword('NoNumbers')).toEqual({
valid: false,
errors: ['Password must contain a number'],
});
});
function validatePassword(password: string) {
const errors: string[] = [];
if (password.length < 8) {
errors.push('Password must be at least 8 characters');
}
if (!/[A-Z]/.test(password)) {
errors.push('Password must contain an uppercase letter');
}
if (!/[0-9]/.test(password)) {
errors.push('Password must contain a number');
}
return { valid: errors.length === 0, errors };
}
const PASSWORD_RULES = [
{ test: (p: string) => p.length >= 8, message: 'Password must be at least 8 characters' },
{ test: (p: string) => /[A-Z]/.test(p), message: 'Password must contain an uppercase letter' },
{ test: (p: string) => /[0-9]/.test(p), message: 'Password must contain a number' },
];
function validatePassword(password: string) {
const errors = PASSWORD_RULES
.filter(rule => !rule.test(password))
.map(rule => rule.message);
return { valid: errors.length === 0, errors };
}
it('should accept valid passwords', () => {
expect(validatePassword('ValidPass1')).toEqual({
valid: true,
errors: [],
});
});
Best Practices
Do
- Write one test at a time
- Run tests after every change
- Test behavior, not implementation
- Keep tests independent
- Use descriptive test names
Don't
- Write tests after code
- Skip the RED phase
- Write multiple tests before implementing
- Test private methods directly
- Couple tests to implementation details
Commands
npm test -- --watch
npm test -- validatePassword
npm test -- --coverage
npm test -- --updateSnapshot
When TDD Might Not Fit
- Exploratory/prototype code
- UI layout (visual testing better)
- Third-party integration spikes
- Performance optimization (profile first)
In these cases, write tests after, but still write them.