Testing Code
Core Workflow
Test writing follows a systematic approach: determine scope, understand patterns, map to requirements, write tests, verify coverage.
- Determine Test Scope
Read project documentation:
-
docs/user-stories/US-###-*.md for acceptance criteria to test
-
docs/feature-spec/F-##-*.md for technical requirements
-
docs/api-contracts.yaml for API specifications
-
Existing test files to understand patterns
Choose test types needed:
-
Unit tests: Individual functions, pure logic, utilities
-
Integration tests: Multiple components working together, API endpoints
-
Component tests: UI components, user interactions
-
E2E tests: Complete user flows, critical paths
-
Contract tests: API request/response validation
-
Performance tests: Load, stress, benchmark testing
- Understand Existing Patterns
Investigate current test approach:
-
Test framework (Jest, Vitest, Pytest, etc.)
-
Mocking patterns and utilities
-
Test data fixtures and setup/teardown
-
Assertion styles
Use code-finder agents if unfamiliar with test structure.
- Map Tests to Requirements
Convert 3-5 acceptance criteria to specific test cases across test types:
Example mapping:
User Story: US-101 User Login
Test Cases
-
Unit: Authentication service
- validateCredentials() returns true for valid email/password
- validateCredentials() returns false for invalid password
- checkAccountStatus() detects locked accounts
-
Integration: Login endpoint
- POST /api/login with valid creds returns 200 + token
- POST /api/login with invalid creds returns 401 + error
- POST /api/login with locked account returns 403
-
Component: Login form
- Submitting form calls login API
- Error message displays on 401 response
- Success redirects to /dashboard
-
E2E: Complete login flow
- User enters credentials → submits → sees dashboard
- User enters wrong password → sees error → retries successfully
-
Write Tests
Unit Test Structure:
describe('AuthService', () => { describe('validateCredentials', () => { it('returns true for valid email and password', async () => { const result = await authService.validateCredentials( 'user@example.com', 'ValidPass123' ); expect(result).toBe(true); });
it('returns false for invalid password', async () => {
const result = await authService.validateCredentials(
'user@example.com',
'WrongPassword'
);
expect(result).toBe(false);
});
}); });
Integration Test Structure:
describe('POST /api/auth/login', () => { beforeEach(async () => { await resetTestDatabase(); await createTestUser({ email: 'test@example.com', password: 'Test123!' }); });
it('returns 200 and token for valid credentials', async () => { const response = await request(app) .post('/api/auth/login') .send({ email: 'test@example.com', password: 'Test123!' });
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('token');
expect(response.body.token).toMatch(/^eyJ/); // JWT format
});
it('returns 401 for invalid password', async () => { const response = await request(app) .post('/api/auth/login') .send({ email: 'test@example.com', password: 'WrongPassword' });
expect(response.status).toBe(401);
expect(response.body.error).toBe('Invalid credentials');
}); });
Component Test Structure:
describe('LoginForm', () => { it('submits form with valid data', async () => { const mockLogin = jest.fn().mockResolvedValue({ success: true }); render(<LoginForm onLogin={mockLogin} />);
await userEvent.type(screen.getByLabelText(/email/i), 'user@example.com');
await userEvent.type(screen.getByLabelText(/password/i), 'Password123');
await userEvent.click(screen.getByRole('button', { name: /log in/i }));
expect(mockLogin).toHaveBeenCalledWith({
email: 'user@example.com',
password: 'Password123'
});
});
it('displays error message on API failure', async () => { const mockLogin = jest.fn().mockRejectedValue(new Error('Invalid credentials')); render(<LoginForm onLogin={mockLogin} />);
await userEvent.type(screen.getByLabelText(/email/i), 'user@example.com');
await userEvent.type(screen.getByLabelText(/password/i), 'wrong');
await userEvent.click(screen.getByRole('button', { name: /log in/i }));
expect(await screen.findByText(/invalid credentials/i)).toBeInTheDocument();
}); });
E2E Test Structure:
test('user can log in successfully', async ({ page }) => { await page.goto('/login');
await page.fill('[name="email"]', 'test@example.com'); await page.fill('[name="password"]', 'Test123!'); await page.click('button:has-text("Log In")');
await page.waitForURL('/dashboard'); expect(page.url()).toContain('/dashboard'); });
- Edge Cases & Error Scenarios
Include boundary conditions and error paths:
describe('Edge cases', () => { it('handles empty email gracefully', async () => { await expect( authService.validateCredentials('', 'password') ).rejects.toThrow('Email is required'); });
it('handles extremely long password', async () => { const longPassword = 'a'.repeat(10000); await expect( authService.validateCredentials('user@example.com', longPassword) ).rejects.toThrow('Password too long'); });
it('handles network timeout', async () => { jest.spyOn(global, 'fetch').mockImplementation( () => new Promise((resolve) => setTimeout(resolve, 10000)) );
await expect(
authService.login('user@example.com', 'pass')
).rejects.toThrow('Request timeout');
}); });
Edge cases to always include:
-
Empty/null inputs
-
Minimum/maximum values
-
Invalid formats
-
Network failures
-
API errors (4xx, 5xx)
-
Timeout conditions
-
Concurrent operations
- Test Data & Fixtures
Create reusable test fixtures:
// tests/fixtures/users.ts export const validUser = { email: 'test@example.com', password: 'Test123!', name: 'Test User' };
export const invalidUsers = { noEmail: { password: 'Test123!' }, noPassword: { email: 'test@example.com' }, invalidEmail: { email: 'not-an-email', password: 'Test123!' }, weakPassword: { email: 'test@example.com', password: '123' } };
// Use in tests import { validUser, invalidUsers } from './fixtures/users';
it('validates user data', () => { expect(validate(validUser)).toBe(true); expect(validate(invalidUsers.noEmail)).toBe(false); });
- Parallel Test Implementation
When tests are independent (different modules, different test types), spawn parallel agents:
Pattern 1: Layer-based
-
Agent 1: Unit tests for services/utilities
-
Agent 2: Integration tests for API endpoints
-
Agent 3: Component tests for UI
-
Agent 4: E2E tests for critical flows
Pattern 2: Feature-based
-
Agent 1: All tests for Feature A
-
Agent 2: All tests for Feature B
-
Agent 3: All tests for Feature C
Pattern 3: Type-based
-
Agent 1: All unit tests
-
Agent 2: All integration tests
-
Agent 3: All E2E tests
- Run & Verify Tests
Execute test suite:
Unit tests
npm test -- --coverage
Integration tests
npm run test:integration
E2E tests
npm run test:e2e
All tests
npm run test:all
Verify coverage:
-
Aim for >80% code coverage
-
100% coverage of critical paths
-
All acceptance criteria have tests
-
All error scenarios tested
Quality Checklist
Coverage:
-
All acceptance criteria from user stories tested
-
Happy path covered
-
Edge cases included
-
Error scenarios tested
-
Boundary conditions validated
Structure:
-
Tests follow existing patterns
-
Clear test descriptions
-
Proper setup/teardown
-
No flaky tests (consistent results)
-
Tests are isolated (no interdependencies)
Data:
-
Test fixtures reusable
-
Database properly seeded/reset
-
Mocks used appropriately
-
No hardcoded test data in production
Integration:
-
Tests run in CI/CD
-
Coverage thresholds enforced
-
Fast feedback (quick tests)
-
Clear failure messages