Initial commit
This commit is contained in:
488
commands/tdd-expert.md
Normal file
488
commands/tdd-expert.md
Normal file
@@ -0,0 +1,488 @@
|
||||
# TDD Expert
|
||||
|
||||
**Description**: Master test-driven development with the Red-Green-Refactor cycle for reliable, maintainable code
|
||||
|
||||
## Core Philosophy
|
||||
|
||||
Test-Driven Development (TDD) is not just about testing - it's a design methodology that leads to better code architecture, fewer bugs, and higher confidence in changes.
|
||||
|
||||
## The Red-Green-Refactor Cycle
|
||||
|
||||
### 🔴 **RED: Write a Failing Test**
|
||||
Write the smallest test that would verify the next piece of functionality
|
||||
- Test should fail for the right reason
|
||||
- Test should be specific and focused
|
||||
- Test should describe desired behavior, not implementation
|
||||
|
||||
### 🟢 **GREEN: Make It Pass**
|
||||
Write the minimal code to make the test pass
|
||||
- Don't worry about perfect code yet
|
||||
- Simplest solution that works
|
||||
- It's OK to hardcode initially
|
||||
|
||||
### 🔵 **REFACTOR: Improve the Code**
|
||||
Clean up both test and production code
|
||||
- Remove duplication
|
||||
- Improve names
|
||||
- Extract functions/classes
|
||||
- Optimize if needed
|
||||
- Tests must still pass
|
||||
|
||||
### ♻️ **REPEAT**
|
||||
Continue the cycle for the next piece of functionality
|
||||
|
||||
## TDD Workflow
|
||||
|
||||
### Starting a New Feature
|
||||
```
|
||||
1. [ ] Write a list of test cases to implement
|
||||
2. [ ] Pick the simplest test case
|
||||
3. [ ] Write a failing test (RED)
|
||||
4. [ ] Run the test - verify it fails
|
||||
5. [ ] Write minimal code to pass (GREEN)
|
||||
6. [ ] Run the test - verify it passes
|
||||
7. [ ] Refactor (REFACTOR)
|
||||
8. [ ] Run tests - verify still passing
|
||||
9. [ ] Pick next test case
|
||||
10. [ ] Repeat
|
||||
```
|
||||
|
||||
### Test Case Brainstorming
|
||||
Before coding, list out test scenarios:
|
||||
```
|
||||
Feature: User Login
|
||||
|
||||
Test Cases:
|
||||
- [ ] Valid credentials → success
|
||||
- [ ] Invalid password → error
|
||||
- [ ] Nonexistent user → error
|
||||
- [ ] Empty email → validation error
|
||||
- [ ] Empty password → validation error
|
||||
- [ ] SQL injection attempt → sanitized
|
||||
- [ ] Rate limiting → too many attempts blocked
|
||||
- [ ] Session creation on success
|
||||
- [ ] Failed attempts logged
|
||||
```
|
||||
|
||||
## Writing Good Tests
|
||||
|
||||
### The AAA Pattern
|
||||
```javascript
|
||||
test('should calculate total price with tax', () => {
|
||||
// ARRANGE: Set up test data
|
||||
const items = [
|
||||
{ price: 10, quantity: 2 },
|
||||
{ price: 5, quantity: 1 }
|
||||
];
|
||||
const taxRate = 0.1;
|
||||
|
||||
// ACT: Execute the code under test
|
||||
const result = calculateTotal(items, taxRate);
|
||||
|
||||
// ASSERT: Verify the result
|
||||
expect(result).toBe(27.5); // (10*2 + 5*1) * 1.1
|
||||
});
|
||||
```
|
||||
|
||||
### Test Naming Conventions
|
||||
```
|
||||
Good test names are descriptive and follow patterns:
|
||||
|
||||
Pattern 1: should_ExpectedBehavior_When_StateUnderTest
|
||||
✓ should_ReturnZero_When_CartIsEmpty
|
||||
✓ should_ThrowError_When_PriceIsNegative
|
||||
|
||||
Pattern 2: When_StateUnderTest_Expect_ExpectedBehavior
|
||||
✓ When_CartIsEmpty_Expect_ZeroTotal
|
||||
✓ When_PriceIsNegative_Expect_Error
|
||||
|
||||
Pattern 3: GivenContext_WhenAction_ThenOutcome
|
||||
✓ GivenEmptyCart_WhenCalculatingTotal_ThenReturnsZero
|
||||
✓ GivenInvalidPrice_WhenCalculating_ThenThrowsError
|
||||
```
|
||||
|
||||
### Test Organization
|
||||
```javascript
|
||||
describe('ShoppingCart', () => {
|
||||
describe('addItem', () => {
|
||||
it('should add item to empty cart', () => { });
|
||||
it('should increment quantity for existing item', () => { });
|
||||
it('should throw error for negative quantity', () => { });
|
||||
});
|
||||
|
||||
describe('removeItem', () => {
|
||||
it('should remove item from cart', () => { });
|
||||
it('should do nothing if item not in cart', () => { });
|
||||
});
|
||||
|
||||
describe('calculateTotal', () => {
|
||||
it('should return 0 for empty cart', () => { });
|
||||
it('should sum all item prices', () => { });
|
||||
it('should apply tax rate correctly', () => { });
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Types of Tests
|
||||
|
||||
### 1. **Unit Tests**
|
||||
Test individual functions/methods in isolation
|
||||
```javascript
|
||||
// Testing a pure function
|
||||
describe('formatCurrency', () => {
|
||||
it('should format USD correctly', () => {
|
||||
expect(formatCurrency(1234.56, 'USD')).toBe('$1,234.56');
|
||||
});
|
||||
|
||||
it('should handle zero', () => {
|
||||
expect(formatCurrency(0, 'USD')).toBe('$0.00');
|
||||
});
|
||||
|
||||
it('should round to 2 decimals', () => {
|
||||
expect(formatCurrency(1.234, 'USD')).toBe('$1.23');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 2. **Integration Tests**
|
||||
Test multiple components working together
|
||||
```javascript
|
||||
// Testing API endpoint with database
|
||||
describe('POST /api/users', () => {
|
||||
it('should create user and return ID', async () => {
|
||||
const userData = { email: 'test@example.com', name: 'Test' };
|
||||
const response = await request(app).post('/api/users').send(userData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body).toHaveProperty('id');
|
||||
|
||||
// Verify in database
|
||||
const user = await db.users.findById(response.body.id);
|
||||
expect(user.email).toBe(userData.email);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 3. **End-to-End Tests**
|
||||
Test complete user workflows
|
||||
```javascript
|
||||
// Testing full user journey
|
||||
describe('User Registration Flow', () => {
|
||||
it('should allow new user to register and login', async () => {
|
||||
// Visit registration page
|
||||
await page.goto('/register');
|
||||
|
||||
// Fill form
|
||||
await page.fill('#email', 'newuser@example.com');
|
||||
await page.fill('#password', 'SecurePass123!');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Should redirect to dashboard
|
||||
await expect(page).toHaveURL('/dashboard');
|
||||
await expect(page.locator('.welcome-message')).toContainText('Welcome');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Test Doubles (Mocks, Stubs, Spies)
|
||||
|
||||
### When to Use Mocks
|
||||
```javascript
|
||||
// Mock external dependencies for unit tests
|
||||
describe('UserService', () => {
|
||||
it('should send welcome email on registration', async () => {
|
||||
// Mock the email service
|
||||
const mockEmailService = {
|
||||
send: jest.fn().mockResolvedValue(true)
|
||||
};
|
||||
|
||||
const userService = new UserService(mockEmailService);
|
||||
await userService.registerUser({ email: 'test@example.com' });
|
||||
|
||||
// Verify email was sent
|
||||
expect(mockEmailService.send).toHaveBeenCalledWith({
|
||||
to: 'test@example.com',
|
||||
template: 'welcome'
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Dependency Injection for Testability
|
||||
```typescript
|
||||
// Bad: Hard to test
|
||||
class UserService {
|
||||
async createUser(data: UserData) {
|
||||
await Database.save(data); // Direct dependency
|
||||
await EmailService.send(data.email); // Hard to mock
|
||||
}
|
||||
}
|
||||
|
||||
// Good: Easy to test
|
||||
class UserService {
|
||||
constructor(
|
||||
private db: DatabaseService,
|
||||
private email: EmailService
|
||||
) {}
|
||||
|
||||
async createUser(data: UserData) {
|
||||
await this.db.save(data);
|
||||
await this.email.send(data.email);
|
||||
}
|
||||
}
|
||||
|
||||
// Now we can inject mocks in tests
|
||||
```
|
||||
|
||||
## TDD Anti-Patterns to Avoid
|
||||
|
||||
### ❌ **Testing Implementation Details**
|
||||
```javascript
|
||||
// Bad: Tests implementation
|
||||
test('should call helper function 3 times', () => {
|
||||
const spy = jest.spyOn(obj, 'helperFunc');
|
||||
obj.mainFunc();
|
||||
expect(spy).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
// Good: Tests behavior
|
||||
test('should return correct result', () => {
|
||||
expect(obj.mainFunc()).toBe(expectedResult);
|
||||
});
|
||||
```
|
||||
|
||||
### ❌ **Overly Complex Tests**
|
||||
```javascript
|
||||
// Bad: Test is harder to understand than the code
|
||||
test('complex test', () => {
|
||||
const data = generateComplexData();
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (data[i].type === 'A') {
|
||||
// ... complex logic
|
||||
}
|
||||
}
|
||||
expect(result).toBe(/* computed value */);
|
||||
});
|
||||
|
||||
// Good: Simple, focused test
|
||||
test('should process type A items correctly', () => {
|
||||
const item = { type: 'A', value: 10 };
|
||||
expect(processItem(item)).toBe(20);
|
||||
});
|
||||
```
|
||||
|
||||
### ❌ **Test Interdependence**
|
||||
```javascript
|
||||
// Bad: Tests depend on each other
|
||||
let userId;
|
||||
test('create user', () => {
|
||||
userId = createUser(); // Side effect
|
||||
});
|
||||
test('update user', () => {
|
||||
updateUser(userId); // Depends on previous test
|
||||
});
|
||||
|
||||
// Good: Each test is independent
|
||||
test('create user', () => {
|
||||
const userId = createUser();
|
||||
expect(userId).toBeDefined();
|
||||
});
|
||||
test('update user', () => {
|
||||
const userId = createUser(); // Setup within test
|
||||
const result = updateUser(userId);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
## Test Coverage Targets
|
||||
|
||||
### Aim for High Coverage, Not 100%
|
||||
- **Statements**: 80%+ (most lines executed)
|
||||
- **Branches**: 75%+ (most if/else paths tested)
|
||||
- **Functions**: 90%+ (most functions tested)
|
||||
- **Lines**: 80%+ (most lines covered)
|
||||
|
||||
### What to Prioritize
|
||||
1. **Critical business logic** - Must have 100% coverage
|
||||
2. **Complex algorithms** - High coverage with edge cases
|
||||
3. **Public APIs** - All entry points tested
|
||||
4. **Error handling** - All error paths verified
|
||||
|
||||
### What's OK to Skip
|
||||
- Trivial getters/setters
|
||||
- Framework boilerplate
|
||||
- Third-party library code
|
||||
- Configuration files
|
||||
|
||||
## TDD Best Practices
|
||||
|
||||
### 1. **Write Tests First** (Truly!)
|
||||
```
|
||||
❌ Bad: Write code first, then add tests
|
||||
✓ Good: Write failing test, then make it pass
|
||||
```
|
||||
|
||||
### 2. **One Assert Per Test** (Usually)
|
||||
```javascript
|
||||
// Prefer focused tests
|
||||
test('should create user with correct email', () => {
|
||||
const user = createUser({ email: 'test@example.com' });
|
||||
expect(user.email).toBe('test@example.com');
|
||||
});
|
||||
|
||||
test('should create user with hashed password', () => {
|
||||
const user = createUser({ password: 'secret' });
|
||||
expect(user.password).not.toBe('secret');
|
||||
expect(user.password).toMatch(/^\$2[aby]\$/);
|
||||
});
|
||||
```
|
||||
|
||||
### 3. **Fast Tests**
|
||||
```
|
||||
- Unit tests: < 10ms each
|
||||
- Integration tests: < 100ms each
|
||||
- E2E tests: < 5s each
|
||||
|
||||
If slower, consider:
|
||||
- Using test doubles
|
||||
- Running in parallel
|
||||
- Optimizing setup/teardown
|
||||
```
|
||||
|
||||
### 4. **Reliable Tests**
|
||||
```
|
||||
Tests should:
|
||||
- ✓ Pass consistently (no flakiness)
|
||||
- ✓ Fail for the right reasons
|
||||
- ✓ Be deterministic
|
||||
- ✓ Not depend on external state
|
||||
- ✓ Clean up after themselves
|
||||
```
|
||||
|
||||
### 5. **Readable Tests**
|
||||
```javascript
|
||||
// Tests are documentation - make them clear
|
||||
test('should allow user to checkout with valid cart', () => {
|
||||
// Arrange: Create a realistic scenario
|
||||
const cart = createCart();
|
||||
cart.addItem({ id: 1, name: 'Widget', price: 9.99 });
|
||||
const user = createUser({ hasPaymentMethod: true });
|
||||
|
||||
// Act: Perform the action
|
||||
const result = checkout(cart, user);
|
||||
|
||||
// Assert: Verify expected outcome
|
||||
expect(result.status).toBe('success');
|
||||
expect(result.orderId).toBeDefined();
|
||||
});
|
||||
```
|
||||
|
||||
## TDD Workflow with Todo System
|
||||
|
||||
```
|
||||
When implementing a feature with TDD:
|
||||
|
||||
Feature: Shopping Cart
|
||||
|
||||
[ ] Write test cases list
|
||||
[ ] RED: Test for adding first item
|
||||
[ ] GREEN: Implement add item (simple)
|
||||
[ ] REFACTOR: Clean up code
|
||||
[ ] RED: Test for adding duplicate item
|
||||
[ ] GREEN: Implement quantity increment
|
||||
[ ] REFACTOR: Extract logic
|
||||
[ ] RED: Test for removing item
|
||||
[ ] GREEN: Implement remove item
|
||||
[ ] REFACTOR: Final cleanup
|
||||
[ ] Verify all tests pass
|
||||
[ ] Check coverage report
|
||||
```
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- When starting any new feature or function
|
||||
- When fixing bugs (write test that reproduces bug first)
|
||||
- When refactoring (tests ensure behavior doesn't change)
|
||||
- When code quality and reliability are critical
|
||||
- When you want to move fast with confidence
|
||||
|
||||
## Example: TDD Session
|
||||
|
||||
```
|
||||
Task: Implement a function to validate email addresses
|
||||
|
||||
TEST LIST:
|
||||
- [ ] Valid email with standard format
|
||||
- [ ] Invalid email without @
|
||||
- [ ] Invalid email without domain
|
||||
- [ ] Empty string
|
||||
- [ ] Null/undefined input
|
||||
- [ ] Email with special characters
|
||||
- [ ] Email with subdomain
|
||||
|
||||
---
|
||||
|
||||
🔴 RED - Test 1:
|
||||
```javascript
|
||||
test('should return true for valid email', () => {
|
||||
expect(isValidEmail('user@example.com')).toBe(true);
|
||||
});
|
||||
// ❌ FAILS: isValidEmail is not defined
|
||||
```
|
||||
|
||||
🟢 GREEN - Minimal implementation:
|
||||
```javascript
|
||||
function isValidEmail(email) {
|
||||
return email === 'user@example.com'; // Hardcoded!
|
||||
}
|
||||
// ✅ PASSES
|
||||
```
|
||||
|
||||
🔴 RED - Test 2:
|
||||
```javascript
|
||||
test('should return false for email without @', () => {
|
||||
expect(isValidEmail('userexample.com')).toBe(false);
|
||||
});
|
||||
// ❌ FAILS: Returns true (because of hardcode)
|
||||
```
|
||||
|
||||
🟢 GREEN - Real implementation:
|
||||
```javascript
|
||||
function isValidEmail(email) {
|
||||
return email.includes('@');
|
||||
}
|
||||
// ✅ PASSES both tests
|
||||
```
|
||||
|
||||
🔴 RED - Test 3:
|
||||
```javascript
|
||||
test('should return false for empty string', () => {
|
||||
expect(isValidEmail('')).toBe(false);
|
||||
});
|
||||
// ❌ FAILS
|
||||
```
|
||||
|
||||
🟢 GREEN - Improved:
|
||||
```javascript
|
||||
function isValidEmail(email) {
|
||||
return email && email.includes('@') && email.includes('.');
|
||||
}
|
||||
// ✅ PASSES all tests
|
||||
```
|
||||
|
||||
🔵 REFACTOR - Polish:
|
||||
```javascript
|
||||
function isValidEmail(email) {
|
||||
if (!email) return false;
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
// ✅ PASSES all tests, cleaner implementation
|
||||
```
|
||||
|
||||
Continue with remaining test cases...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Remember**: TDD feels slower at first, but pays dividends in fewer bugs, better design, and faster debugging. Trust the process!
|
||||
Reference in New Issue
Block a user