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
code
┌──────────────────────────────────────────────────────────┐ │ │ │ 🔴 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
typescriptdescribe('calculateDiscount', () => { it('should return 0 for orders under $50', () => { expect(calculateDiscount(30)).toBe(0); }); }); - •
Run the test - it MUST fail
bashnpm test -- calculateDiscount
Phase 2: GREEN - Make It Pass
- •
Write the minimal code to pass
typescriptfunction calculateDiscount(orderTotal: number): number { return 0; } - •
Run the test - it MUST pass
bashnpm 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:
typescript
it('should return 10% for orders $50-$100', () => {
expect(calculateDiscount(75)).toBe(7.5);
});
Test Structure
Arrange-Act-Assert Pattern
typescript
it('should apply discount to premium users', () => {
// Arrange
const user = createUser({ isPremium: true });
const order = createOrder({ total: 100 });
// Act
const result = applyDiscount(user, order);
// Assert
expect(result.total).toBe(90);
expect(result.discountApplied).toBe(10);
});
Test Naming Convention
typescript
// Format: should_[expected behavior]_when_[condition]
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
typescript
// Step 1: RED - First test
describe('validatePassword', () => {
it('should reject passwords shorter than 8 characters', () => {
expect(validatePassword('Short1')).toEqual({
valid: false,
errors: ['Password must be at least 8 characters'],
});
});
});
// Step 2: GREEN - Minimal implementation
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 };
}
// Step 3: RED - Next test
it('should reject passwords without uppercase', () => {
expect(validatePassword('lowercase1')).toEqual({
valid: false,
errors: ['Password must contain an uppercase letter'],
});
});
// Step 4: GREEN - Add uppercase check
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 };
}
// Step 5: RED - Test for number requirement
it('should reject passwords without numbers', () => {
expect(validatePassword('NoNumbers')).toEqual({
valid: false,
errors: ['Password must contain a number'],
});
});
// Step 6: GREEN - Add number check
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 };
}
// Step 7: REFACTOR - Clean up
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 };
}
// Step 8: RED - Happy path test
it('should accept valid passwords', () => {
expect(validatePassword('ValidPass1')).toEqual({
valid: true,
errors: [],
});
});
// Already passes! ✅
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
bash
# Run tests in watch mode npm test -- --watch # Run specific test file npm test -- validatePassword # Run with coverage npm test -- --coverage # Run and update snapshots 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.