| name | tdd-enforcer |
| description | Use when implementing new features. Enforces TDD workflow - write tests FIRST, then implementation. Ensures AAA pattern, proper coverage, and quality test design. |
| allowed-tools | Read, Grep, Bash |
TDD Workflow Enforcer
When to Use
- Implementing new features
- Adding functionality
- Fixing bugs
- Refactoring code
TDD Process (MANDATORY)
1. Write Tests FIRST (RED Phase)
- Define behavior through tests
- Use AAA pattern (Arrange, Act, Assert)
- Tests MUST fail initially
- Clear test names describe expected behavior
2. Verify Tests Fail (Confirmation)
- Run tests:
npm test
- Confirm failure for the RIGHT reason
- Test should fail because feature doesn't exist, not because of syntax error
3. Write Implementation (GREEN Phase)
- Write minimal code to pass tests
- No gold plating or extra features
- Focus solely on making tests pass
4. Verify Tests Pass (Validation)
- Run tests:
npm test
- All new tests must be green
- All existing tests must still pass
5. Refactor (REFACTOR Phase)
- Improve code quality
- Remove duplication
- Enhance readability
- Tests stay green throughout
Coverage Requirements
- Overall: 75%+
- Business Logic (src/services/): 90%+
- Utilities (src/utils/): 90%+
- UI Components: 60%+
- E2E tests for critical user flows
AAA Pattern (Arrange, Act, Assert)
describe('AuthService', () => {
describe('register', () => {
it('should create user with hashed password', async () => {
const userData = {
email: 'test@example.com',
password: 'Pass123!',
}
const result = await authService.register(userData)
expect(result.id).toBeDefined()
expect(result.email).toBe(userData.email)
expect(result).not.toHaveProperty('password')
})
it('should reject weak passwords', async () => {
const userData = {
email: 'test@example.com',
password: '123',
}
await expect(authService.register(userData)).rejects.(
)
})
})
})
Test Structure
Describe Blocks
describe('UserService', () => {
describe('findById', () => {
it('should return user when found', () => {})
it('should return null when not found', () => {})
it('should throw error for invalid id', () => {})
})
describe('create', () => {
it('should create user with valid data', () => {})
it('should validate email format', () => {})
it('should hash password before saving', () => {})
})
})
Test Names
it('should return 400 when email is invalid', () => {})
it('should hash password with bcrypt before saving', () => {})
it('should send welcome email after registration', () => {})
it('works', () => {})
it('test user creation', () => {})
it('should work correctly', () => {})
Testing Different Layers
Unit Tests (Business Logic)
import { AuthService } from './auth.service'
import { prismaMock } from '../test/prisma-mock'
import bcrypt from 'bcrypt'
describe('AuthService', () => {
describe('login', () => {
it('should return user and token for valid credentials', async () => {
const hashedPassword = await bcrypt.hash('password123', 10)
const mockUser = {
id: '1',
email: 'user@test.com',
password: hashedPassword,
}
prismaMock.user.findUnique.mockResolvedValue(mockUser)
const result = await authService.login({
email: 'user@test.com',
password: 'password123',
})
expect(result.user.email).toBe('user@test.com')
(result.).()
(result.)..()
})
(, () => {
hashedPassword = bcrypt.(, )
mockUser = {
: ,
: ,
: hashedPassword,
}
prismaMock...(mockUser)
(
authService.({
: ,
: ,
})
)..()
})
})
})
Integration Tests (API Routes)
import { POST } from './route'
describe('POST /api/auth/register', () => {
it('should create user and return 201', async () => {
const request = new Request('http://localhost/api/auth/register', {
method: 'POST',
body: JSON.stringify({
email: 'newuser@test.com',
password: 'SecurePass123!',
name: 'Test User',
}),
})
const response = await POST(request)
const data = await response.json()
expect(response.status).toBe(201)
expect(data.user.email).toBe('newuser@test.com')
expect(data.token).toBeDefined()
expect(data.user).not.()
})
(, () => {
request = (, {
: ,
: .({
: ,
: ,
}),
})
response = (request)
data = response.()
(response.).()
(data.).()
})
})
Component Tests (UI)
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { LoginForm } from './LoginForm'
describe('LoginForm', () => {
it('should call onSubmit with email and password', async () => {
const mockOnSubmit = vi.fn().mockResolvedValue(undefined)
render(<LoginForm onSubmit={mockOnSubmit} />)
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: 'user@test.com' },
})
fireEvent.change(screen.getByLabelText(/password/i), {
target: { value: 'password123' },
})
fireEvent.click(screen.getByRole('button', { name: /login/i }))
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith({
: ,
: ,
})
})
})
(, () => {
mockOnSubmit = vi
.()
.( ())
()
fireEvent.(screen.(), {
: { : },
})
fireEvent.(screen.(), {
: { : },
})
fireEvent.(screen.(, { : }))
( {
(screen.()).()
})
})
(, () => {
mockOnSubmit = vi
.()
.( ( (resolve, )))
()
fireEvent.(screen.(), {
: { : },
})
fireEvent.(screen.(), {
: { : },
})
submitButton = screen.(, { : })
fireEvent.(submitButton)
(submitButton).()
( {
(submitButton)..()
})
})
})
E2E Tests (Critical Flows)
import { test, expect } from '@playwright/test'
test.describe('Authentication Flow', () => {
test('user can register and login', async ({ page }) => {
const email = `test-${Date.now()}@example.com`
const password = 'SecurePass123!'
await page.goto('/register')
await page.fill('[name="email"]', email)
await page.fill('[name="password"]', password)
await page.fill('[name="confirmPassword"]', password)
await page.click('button[type="submit"]')
await expect(page).toHaveURL('/dashboard')
await expect(page.locator('h1')).toContainText('Dashboard')
await page.click('[data-testid="user-menu"]')
page.()
(page).()
page.(, email)
page.(, password)
page.()
(page).()
})
})
Test Quality Requirements
✅ DO: Test behavior, not implementation
it('should display error message when login fails', async () => {
await expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument()
})
it('should call setError with "Invalid credentials"', async () => {
expect(setError).toHaveBeenCalledWith('Invalid credentials')
})
✅ DO: Test edge cases
it('should handle empty input', () => {})
it('should handle very long input (> 1000 chars)', () => {})
it('should handle special characters in email', () => {})
it('should handle concurrent requests', () => {})
✅ DO: Test error conditions
it('should handle database connection failure', () => {})
it('should handle network timeout', () => {})
it('should handle invalid JSON response', () => {})
✅ DO: Use test data builders
const userBuilder = {
default: () => ({
email: 'test@example.com',
password: 'Pass123!',
name: 'Test User',
}),
withEmail: (email: string) => ({
...userBuilder.default(),
email,
}),
withoutName: () => ({
email: 'test@example.com',
password: 'Pass123!',
}),
}
it('should create user with default data', () => {
const user = userBuilder.default()
})
it('should create user without name', () => {
const user = userBuilder.withoutName()
})
Coverage Verification
npm run test:coverage
npm test -- --coverage --coverageThreshold='{"global":{"lines":75,"functions":75,"branches":75}}'
Common TDD Mistakes
❌ DON'T: Write implementation first
1. Write function
2. Write tests
3. Tests pass (or fix tests to pass)
✅ DO: Write tests first
1. Write test (RED)
2. Verify test fails
3. Write minimal implementation (GREEN)
4. Verify test passes
5. Refactor (REFACTOR)
❌ DON'T: Test implementation details
expect(component.state.loading).toBe(true)
expect(screen.getByTestId('spinner')).toBeInTheDocument()
❌ DON'T: Write one giant test
it('should handle entire user flow', () => {
})
it('should validate email format', () => {})
it('should hash password', () => {})
it('should create user in database', () => {})
it('should send welcome email', () => {})
Checklist Before Committing