Testing Workflow
Complete guide to achieving comprehensive test coverage (80%+) using agentful's systematic testing workflow.
Overview
The Testing workflow ensures code quality through comprehensive testing including unit tests, integration tests, and E2E tests. It achieves and maintains ≥80% coverage across the codebase.
What This Workflow Delivers
- ✅ Unit tests for all services and components
- ✅ Integration tests for API endpoints
- ✅ E2E tests for critical user flows
- ✅ Test coverage ≥ 80%
- ✅ Fast, reliable test suites
- ✅ Regression prevention
Typical Timeline
| Testing Scope | Time | Test Count |
|---|---|---|
| Component (single component) | 5-15 minutes | 3-8 tests |
| Service (single service) | 10-20 minutes | 8-15 tests |
| Feature (complete feature) | 30-60 minutes | 20-40 tests |
| Suite (multiple features) | 1-4 hours | 50-150 tests |
Prerequisites
Before starting testing, ensure:
1. Code to Test is Available
# Code exists
ls src/
# Dependencies installed
npm install
# Can run tests
npm test -- --passWithNoTests2. Testing Framework Configured
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'jsdom', // For component tests
setupFiles: './src/test/setup.ts',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'src/test/',
'**/*.d.ts',
],
},
},
});3. Test Utilities Available
// src/test/setup.ts
import { vi } from 'vitest';
import '@testing-library/jest-dom';
// Global mocks
vi.mock('next/navigation', () => ({
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
}),
useSearchParams: () => new URLSearchParams(),
}));
// Test utilities
export function createMockUser(overrides = {}) {
return {
id: '123',
email: 'test@example.com',
name: 'Test User',
...overrides,
};
}The Testing Loop
Complete Workflow Diagram
┌─────────────────────────────────────────────────────────────┐
│ START: Testing Goal Defined │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 1. TEST STRATEGY │
│ • Identify what needs testing │
│ • Categorize by type (unit/integration/E2E) │
│ • Determine coverage gaps │
│ • Prioritize critical paths │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 2. UNIT TESTS │
│ • Test individual functions │
│ • Test component rendering │
│ • Test hooks behavior │
│ • Mock external dependencies │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 3. INTEGRATION TESTS │
│ • Test API endpoints │
│ • Test module interactions │
│ • Test database operations │
│ • Test data flow │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 4. E2E TESTS │
│ • Test complete user flows │
│ • Test cross-feature scenarios │
│ • Test real browser interactions │
│ • Test critical paths │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 5. COVERAGE ANALYSIS │
│ • Run coverage report │
│ • Identify uncovered code │
│ • Add tests for gaps │
│ • Reach 80% threshold │
└─────────────────────────────────────────────────────────────┘
↓
Coverage < 80%?
│
┌──────────┴──────────┐
↓ ↓
┌─────────────────────────┐ ┌─────────────────────────┐
│ 6a. ADD MORE TESTS │ │ 6b. COVERAGE MET │
│ • Identify gaps │ │ • All critical paths │
│ • Test edge cases │ │ • ≥80% coverage │
│ • Re-measure │ │ • All tests passing │
└─────────────────────────┘ └─────────────────────────┘
│ │
└──────────┬──────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 7. VALIDATION │
│ • All tests pass │
│ • Coverage threshold met │
│ • Tests are deterministic │
│ • Tests run fast (<5 seconds for unit) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 8. COMPLETION │
│ • Update completion.json │
│ • Document test suite │
│ • Set up CI/CD integration │
│ • Mark testing complete │
└─────────────────────────────────────────────────────────────┘Step-by-Step Guide
Step 1: Test Strategy
Agent: @orchestrator Time: 5-10 minutes
Strategy creation:## Test Strategy: User Authentication
### Testing Scope
**Modules to Test:**
1. AuthService (src/services/auth.service.ts)
2. TokenService (src/services/token.service.ts)
3. LoginForm (src/components/auth/LoginForm.tsx)
4. Login API (src/app/api/auth/login/route.ts)
5. Registration flow (E2E)
**Test Categories:**
#### 1. Unit Tests (40 tests)
- AuthService: 15 tests
- login() with valid credentials
- login() with invalid credentials
- login() with non-existent user
- register() with valid data
- register() with duplicate email
- register() with weak password
- password validation
- email validation
- token generation
- token verification
- account locking
- failed login tracking
- logout functionality
- session management
- error handling
- TokenService: 10 tests
- generateAccessToken()
- generateRefreshToken()
- verifyAccessToken()
- verifyRefreshToken()
- token expiration
- invalid token handling
- token payload encoding
- token signature validation
- token refresh flow
- token revocation
- LoginForm Component: 10 tests
- renders email input
- renders password input
- renders submit button
- shows validation errors
- disables button while loading
- calls login on submit
- redirects on success
- shows error message
- handles network errors
- accessible form labels
- useAuth Hook: 5 tests
- returns user state
- returns login function
- returns logout function
- updates user on login
- clears user on logout
#### 2. Integration Tests (8 tests)
- POST /api/auth/login
- success with valid credentials
- 401 with invalid credentials
- 400 with missing fields
- returns token on success
- returns user data
- rate limiting
- account lockout
- session creation
#### 3. E2E Tests (3 tests)
- Registration flow
- navigate to register
- fill form
- submit
- verify redirect
- verify login
- Login flow
- navigate to login
- fill credentials
- submit
- verify redirect to dashboard
- verify user displayed
- Logout flow
- login
- click logout
- verify redirect to login
- verify session cleared
### Coverage Goals
**Target:** 87% coverage
**Current:** 45% coverage
**Gap:** +42 percentage points
**Priority Areas:**
1. Critical paths (login, register) - 100% coverage
2. Error handling - 95% coverage
3. Edge cases - 80% coverage
4. UI components - 85% coverage
### Success Criteria
- ✅ All 51 tests passing
- ✅ 87% coverage (≥80% threshold)
- ✅ No flaky tests
- ✅ Unit tests run in <3 seconds
- ✅ Integration tests run in <10 seconds
- ✅ E2E tests run in <30 secondsStep 2: Unit Tests
Agent: @tester Time: 30-45 minutes
Service Tests
// src/services/__tests__/auth.service.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { AuthService } from '../auth.service';
import { UserRepository } from '../../repositories/user.repository';
import { TokenService } from '../token.service';
describe('AuthService', () => {
let service: AuthService;
let mockUserRepo: UserRepository;
let mockTokenService: TokenService;
beforeEach(() => {
// Create mocks
mockUserRepo = {
findByEmail: vi.fn(),
create: vi.fn(),
update: vi.fn(),
} as any;
mockTokenService = {
createTokenPair: vi.fn(),
verifyAccessToken: vi.fn(),
} as any;
service = new AuthService(mockUserRepo, mockTokenService);
});
describe('login', () => {
it('should authenticate user with valid credentials', async () => {
// Arrange
const user = createMockUser({
id: '123',
email: 'test@example.com',
password: 'hashed-password',
});
mockUserRepo.findByEmail.mockResolvedValue(user);
mockTokenService.createTokenPair.mockResolvedValue({
accessToken: 'access-token',
refreshToken: 'refresh-token',
});
// Act
const result = await service.login('test@example.com', 'password123');
// Assert
expect(result.user).toEqual(user);
expect(result.accessToken).toBe('access-token');
expect(result.refreshToken).toBe('refresh-token');
expect(mockUserRepo.findByEmail).toHaveBeenCalledWith('test@example.com');
expect(mockTokenService.createTokenPair).toHaveBeenCalledWith(user);
});
it('should throw error for non-existent user', async () => {
// Arrange
mockUserRepo.findByEmail.mockResolvedValue(null);
// Act & Assert
await expect(
service.login('missing@example.com', 'password123')
).rejects.toThrow('Invalid credentials');
expect(mockUserRepo.findByEmail).toHaveBeenCalledWith('missing@example.com');
expect(mockTokenService.createTokenPair).not.toHaveBeenCalled();
});
it('should throw error for invalid password', async () => {
// Arrange
const user = createMockUser({
password: 'hashed-password',
});
mockUserRepo.findByEmail.mockResolvedValue(user);
mockUserRepo.findByEmail.mockImplementation(async (email: string) => {
if (email === 'test@example.com') {
// Simulate password verification failure
const userWithWrongPassword = { ...user };
(userWithWrongPassword as any).verifyPassword = vi.fn().mockResolvedValue(false);
return userWithWrongPassword;
}
return null;
});
// Act & Assert
await expect(
service.login('test@example.com', 'wrongpassword')
).rejects.toThrow('Invalid credentials');
});
it('should handle account lockout', async () => {
// Arrange
const lockedUser = createMockUser({
lockedUntil: new Date(Date.now() + 3600000), // Locked for 1 hour
});
mockUserRepo.findByEmail.mockResolvedValue(lockedUser);
// Act & Assert
await expect(
service.login('locked@example.com', 'password123')
).rejects.toThrow('Account locked');
expect(mockTokenService.createTokenPair).not.toHaveBeenCalled();
});
it('should update last login on success', async () => {
// Arrange
const user = createMockUser();
mockUserRepo.findByEmail.mockResolvedValue(user);
mockTokenService.createTokenPair.mockResolvedValue({
accessToken: 'token',
refreshToken: 'refresh',
});
mockUserRepo.update.mockResolvedValue(user);
// Act
await service.login('test@example.com', 'password123');
// Assert
expect(mockUserRepo.update).toHaveBeenCalledWith(user.id, {
lastLoginAt: expect.any(Date),
});
});
});
describe('register', () => {
it('should create new user with hashed password', async () => {
// Arrange
const userData = {
email: 'new@example.com',
password: 'password123',
name: 'New User',
};
const createdUser = createMockUser(userData);
mockUserRepo.findByEmail.mockResolvedValue(null); // No existing user
mockUserRepo.create.mockResolvedValue(createdUser);
// Act
const result = await service.register(userData);
// Assert
expect(result).toEqual(createdUser);
expect(mockUserRepo.findByEmail).toHaveBeenCalledWith(userData.email);
expect(mockUserRepo.create).toHaveBeenCalledWith({
email: userData.email,
password: expect.any(String), // Hashed password
name: userData.name,
});
expect(mockUserRepo.create).toHaveBeenCalledWith(
expect.objectContaining({
password: expect.not.toBe(userData.password), // Password was hashed
})
);
});
it('should throw error for duplicate email', async () => {
// Arrange
const existingUser = createMockUser({
email: 'existing@example.com',
});
mockUserRepo.findByEmail.mockResolvedValue(existingUser);
// Act & Assert
await expect(
service.register({
email: 'existing@example.com',
password: 'password123',
name: 'User',
})
).rejects.toThrow('User already exists');
expect(mockUserRepo.create).not.toHaveBeenCalled();
});
it('should validate password strength', async () => {
// Arrange
mockUserRepo.findByEmail.mockResolvedValue(null);
// Act & Assert
await expect(
service.register({
email: 'test@example.com',
password: 'weak', // Too short
name: 'Test User',
})
).rejects.toThrow('Password must be at least 8 characters');
});
});
describe('verifyToken', () => {
it('should verify valid access token', async () => {
// Arrange
const payload = { userId: '123', email: 'test@example.com' };
mockTokenService.verifyAccessToken.mockReturnValue(payload);
// Act
const result = service.verifyToken('valid-token');
// Assert
expect(result).toEqual(payload);
expect(mockTokenService.verifyAccessToken).toHaveBeenCalledWith('valid-token');
});
it('should throw error for invalid token', async () => {
// Arrange
mockTokenService.verifyAccessToken.mockImplementation(() => {
throw new Error('Invalid token');
});
// Act & Assert
expect(() => {
service.verifyToken('invalid-token');
}).toThrow('Invalid token');
});
});
});
// Helper function
function createMockUser(overrides = {}) {
return {
id: '123',
email: 'test@example.com',
password: 'hashed-password',
name: 'Test User',
createdAt: new Date(),
updatedAt: new Date(),
lastLoginAt: null,
lockedUntil: null,
failedLoginAttempts: 0,
verifyPassword: vi.fn().mockResolvedValue(true),
...overrides,
};
}Component Tests
// src/components/__tests__/LoginForm.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from '../LoginForm';
describe('LoginForm', () => {
it('should render email and password inputs', () => {
render(<LoginForm />);
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument();
});
it('should show validation errors for empty fields', async () => {
const user = userEvent.setup();
render(<LoginForm />);
// Submit without filling fields
const submitButton = screen.getByRole('button', { name: /sign in/i });
await user.click(submitButton);
// Wait for validation errors
await waitFor(() => {
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
expect(screen.getByText(/password is required/i)).toBeInTheDocument();
});
});
it('should show validation error for invalid email', async () => {
const user = userEvent.setup();
render(<LoginForm />);
const emailInput = screen.getByLabelText(/email/i);
await user.type(emailInput, 'invalid-email');
const submitButton = screen.getByRole('button', { name: /sign in/i });
await user.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/invalid email format/i)).toBeInTheDocument();
});
});
it('should call login with correct credentials', async () => {
const mockLogin = vi.fn().mockResolvedValue({
user: { id: '123', email: 'test@example.com' },
});
const user = userEvent.setup();
render(<LoginForm onLogin={mockLogin} />);
// Fill form
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
await user.type(screen.getByLabelText(/password/i), 'password123');
// Submit
await user.click(screen.getByRole('button', { name: /sign in/i }));
// Assert
await waitFor(() => {
expect(mockLogin).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
});
it('should disable submit button while loading', async () => {
const mockLogin = vi.fn(() => new Promise(resolve => setTimeout(resolve, 1000)));
const user = userEvent.setup();
render(<LoginForm onLogin={mockLogin} />);
// Fill and submit
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
await user.type(screen.getByLabelText(/password/i), 'password123');
const submitButton = screen.getByRole('button', { name: /sign in/i });
await user.click(submitButton);
// Button should be disabled and show loading
await waitFor(() => {
expect(submitButton).toBeDisabled();
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
});
it('should display error message on login failure', async () => {
const mockLogin = vi.fn().mockRejectedValue(new Error('Invalid credentials'));
const user = userEvent.setup();
render(<LoginForm onLogin={mockLogin} />);
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
await user.type(screen.getByLabelText(/password/i), 'wrongpassword');
await user.click(screen.getByRole('button', { name: /sign in/i }));
await waitFor(() => {
expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument();
});
});
it('should be accessible with proper labels', () => {
render(<LoginForm />);
expect(screen.getByLabelText(/email/i)).toHaveAttribute('type', 'email');
expect(screen.getByLabelText(/password/i)).toHaveAttribute('type', 'password');
expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument();
});
});Hook Tests
// src/hooks/__tests__/useAuth.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, act, waitFor } from '@testing-library/react';
import { useAuth } from '../useAuth';
// Mock fetch
global.fetch = vi.fn();
describe('useAuth', () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
});
it('should return initial state', () => {
const { result } = renderHook(() => useAuth());
expect(result.current.user).toBeNull();
expect(result.current.isLoading).toBe(true);
expect(result.current.isAuthenticated).toBe(false);
});
it('should login user and update state', async () => {
const mockUser = { id: '123', email: 'test@example.com' };
(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => ({ user: mockUser, token: 'access-token' }),
});
const { result } = renderHook(() => useAuth());
// Wait for initial check
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
// Login
await act(async () => {
await result.current.login('test@example.com', 'password123');
});
// Assert
expect(result.current.user).toEqual(mockUser);
expect(result.current.isAuthenticated).toBe(true);
expect(global.fetch).toHaveBeenCalledWith('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'test@example.com',
password: 'password123',
}),
});
});
it('should logout user and clear state', async () => {
const { result } = renderHook(() => useAuth());
// Set initial user
await act(async () => {
await result.current.login('test@example.com', 'password123');
});
expect(result.current.isAuthenticated).toBe(true);
// Logout
await act(async () => {
await result.current.logout();
});
// Assert
expect(result.current.user).toBeNull();
expect(result.current.isAuthenticated).toBe(false);
});
it('should handle login errors', async () => {
(global.fetch as any).mockRejectedValueOnce(new Error('Network error'));
const { result } = renderHook(() => useAuth());
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
await expect(
act(async () => {
await result.current.login('test@example.com', 'password123');
})
).rejects.toThrow('Network error');
expect(result.current.user).toBeNull();
expect(result.current.isAuthenticated).toBe(false);
});
});🧪 Tester Agent writing unit tests...
Created unit tests:
✓ src/services/__tests__/auth.service.test.ts (18 tests)
✓ src/services/__tests__/token.service.test.ts (10 tests)
✓ src/components/__tests__/LoginForm.test.tsx (8 tests)
✓ src/hooks/__tests__/useAuth.test.ts (5 tests)
Running unit tests...
✓ 41 tests passed
✓ Coverage: 73%
Time: 18 minutesStep 3: Integration Tests
Agent: @tester Time: 15-25 minutes
// src/app/api/auth/__tests__/login.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import request from 'supertest';
import { app } from '../../../app';
describe('POST /api/auth/login', () => {
let testUserId: string;
// Setup: Create test user
beforeAll(async () => {
const res = await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
password: 'password123',
name: 'Test User',
});
testUserId = res.body.id;
});
// Cleanup: Delete test user
afterAll(async () => {
await request(app).delete(`/api/users/${testUserId}`);
});
it('should login with valid credentials', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'password123',
});
expect(res.status).toBe(200);
expect(res.body).toHaveProperty('user');
expect(res.body).toHaveProperty('accessToken');
expect(res.body).toHaveProperty('refreshToken');
expect(res.body.user).toHaveProperty('email', 'test@example.com');
expect(res.body.user).not.toHaveProperty('password');
});
it('should reject invalid credentials', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'wrongpassword',
});
expect(res.status).toBe(401);
expect(res.body).toHaveProperty('error', 'Invalid credentials');
expect(res.body).not.toHaveProperty('accessToken');
});
it('should reject non-existent user', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({
email: 'nonexistent@example.com',
password: 'password123',
});
expect(res.status).toBe(401);
expect(res.body).toHaveProperty('error');
});
it('should validate required fields', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
// Missing password
});
expect(res.status).toBe(400);
expect(res.body).toHaveProperty('error');
});
it('should validate email format', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({
email: 'invalid-email',
password: 'password123',
});
expect(res.status).toBe(400);
expect(res.body).toHaveProperty('error');
});
it('should handle rate limiting', async () => {
// Attempt 5 failed logins rapidly
const promises = Array(5).fill(null).map(() =>
request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'wrongpassword',
})
);
const results = await Promise.all(promises);
// Last attempt should be rate limited
expect(results[4].status).toBe(429);
expect(results[4].body).toHaveProperty('error', 'Too many attempts');
});
it('should lock account after multiple failed attempts', async () => {
// Attempt 10 failed logins
for (let i = 0; i < 10; i++) {
await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'wrongpassword',
});
}
// Account should be locked
const res = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'password123', // Correct password
});
expect(res.status).toBe(423); // Locked
expect(res.body).toHaveProperty('error', 'Account locked');
});
});🧪 Tester Agent writing integration tests...
Created integration tests:
✓ src/app/api/auth/__tests__/login.test.ts (7 tests)
✓ src/app/api/auth/__tests__/register.test.ts (5 tests)
Running integration tests...
✓ 12 tests passed
✓ Coverage: 81%
Time: 12 minutesStep 4: E2E Tests
Agent: @tester Time: 15-20 minutes
// e2e/auth.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Authentication Flow', () => {
test.beforeEach(async ({ page }) => {
// Clear storage before each test
await page.context().clearCookies();
await page.evaluate(() => localStorage.clear());
});
test('should register new user', async ({ page }) => {
await page.goto('/register');
// Fill registration form
await page.fill('[name="email"]', `test-${Date.now()}@example.com`);
await page.fill('[name="password"]', 'password123');
await page.fill('[name="name"]', 'Test User');
await page.fill('[name="confirmPassword"]', 'password123');
// Submit
await page.click('button[type="submit"]');
// Should redirect to dashboard
await expect(page).toHaveURL('/dashboard');
// Should show welcome message
await expect(page.locator('text=Welcome')).toBeVisible();
});
test('should login with valid credentials', async ({ page }) => {
await page.goto('/login');
// Fill login form
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="password"]', 'password123');
// Submit
await page.click('button[type="submit"]');
// Should redirect to dashboard
await expect(page).toHaveURL('/dashboard', { timeout: 5000 });
// Should show user menu
await expect(page.locator('[aria-label="User menu"]')).toBeVisible();
});
test('should show error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="password"]', 'wrongpassword');
await page.click('button[type="submit"]');
// Should stay on login page
await expect(page).toHaveURL('/login');
// Should show error message
await expect(page.locator('text=Invalid credentials')).toBeVisible();
});
test('should logout and redirect to login', async ({ page }) => {
// Login first
await page.goto('/login');
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
// Wait for dashboard
await expect(page).toHaveURL('/dashboard');
// Logout
await page.click('[aria-label="User menu"]');
await page.click('text=Logout');
// Should redirect to login
await expect(page).toHaveURL('/login');
// Should not show user menu
await expect(page.locator('[aria-label="User menu"]')).not.toBeVisible();
});
test('should persist session across page reloads', async ({ page }) => {
// Login
await page.goto('/login');
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
// Reload page
await page.reload();
// Should still be logged in
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[aria-label="User menu"]')).toBeVisible();
});
test('should validate email format', async ({ page }) => {
await page.goto('/register');
await page.fill('[name="email"]', 'invalid-email');
await page.fill('[name="password"]', 'password123');
await page.fill('[name="name"]', 'Test User');
await page.click('button[type="submit"]');
// Should show validation error
await expect(page.locator('text=Invalid email format')).toBeVisible();
// Should not submit
await expect(page).toHaveURL('/register');
});
test('should require password confirmation', async ({ page }) => {
await page.goto('/register');
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="password"]', 'password123');
await page.fill('[name="confirmPassword"]', 'different');
await page.click('button[type="submit"]');
// Should show error
await expect(page.locator('text=Passwords do not match')).toBeVisible();
});
});🧪 Tester Agent writing E2E tests...
Created E2E tests:
✓ e2e/auth.spec.ts (7 tests)
Running E2E tests...
✓ 7 tests passed
✓ All user flows working
Time: 15 minutesStep 5: Coverage Analysis
Agent: @reviewer Time: 2-3 minutes
npm test -- --coverage----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 87.3 | 84.2 | 89.1 | 87.3 |
auth.service.ts | 94.2 | 92.5 | 100 | 94.5 | 45,78
token.service.ts | 91.8 | 88.3 | 100 | 91.2 | 23
useAuth.ts | 85.7 | 80.0 | 85.7 | 85.7 | 12-15
LoginForm.tsx | 82.1 | 75.0 | 83.3 | 82.1 | 28-32
----------|---------|----------|---------|---------|-------------------
Coverage threshold: 80%
Actual coverage: 87.3% ✅
Gaps identified:
1. auth.service.ts:45 - Account lockout edge case
2. auth.service.ts:78 - Token refresh error handling
3. useAuth.ts:12-15 - Session initialization error
4. LoginForm.tsx:28-32 - Accessibility edge case// Add tests for uncovered lines
// auth.service.ts:45 - Account lockout edge case
it('should unlock account after lockout period expires', async () => {
const user = createMockUser({
lockedUntil: new Date(Date.now() - 1000), // Expired 1 second ago
});
mockUserRepo.findByEmail.mockResolvedValue(user);
const result = await service.login('test@example.com', 'password123');
expect(result.user).toEqual(user);
expect(mockUserRepo.update).toHaveBeenCalledWith(user.id, {
lockedUntil: null,
failedLoginAttempts: 0,
});
});----------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
----------|---------|----------|---------|---------|
All files | 91.5 | 88.7 | 92.3 | 91.5 |
----------|---------|----------|---------|---------|
✅ Coverage goal achieved: 91.5% (target: 80%)Step 6: Validation
Agent: @reviewer Time: 1-2 minutes
# All tests pass
npm test
✓ 60 tests passed
# Coverage met
npm test -- --coverage
✓ Coverage: 91.5% (≥80% threshold)
# Tests are fast
npm test -- --reporter=verbose
✓ Unit tests: 2.8s
✓ Integration tests: 8.2s
✓ E2E tests: 24.1s
# No flaky tests
npm test -- --repeat=10
✓ All tests passed on all 10 runsFinal Results
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Testing Workflow Complete
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Module: User Authentication
Time: 1 hour 12 minutes
Tests Created:
• Unit tests: 41 ( AuthService: 18, TokenService: 10, Components: 13)
• Integration tests: 12 (Login: 7, Register: 5)
• E2E tests: 7 (Complete flows)
Total Tests: 60
All Passing: ✅
Coverage: 91.5% (target: 80%)
Improvement: +46.5 percentage points
Quality Metrics:
• Unit test speed: 2.8s ✅ (<3s target)
• Integration test speed: 8.2s ✅ (<10s target)
• E2E test speed: 24.1s ✅ (<30s target)
• Flaky tests: 0 ✅
• Deterministic: ✅
Test Files Created:
✓ src/services/__tests__/auth.service.test.ts
✓ src/services/__tests__/token.service.test.ts
✓ src/components/__tests__/LoginForm.test.tsx
✓ src/hooks/__tests__/useAuth.test.ts
✓ src/app/api/auth/__tests__/login.test.ts
✓ src/app/api/auth/__tests__/register.test.ts
✓ e2e/auth.spec.ts
Ready for CI/CD integration!Test Organization
File Structure
src/
├── services/
│ ├── auth.service.ts
│ └── __tests__/
│ └── auth.service.test.ts
├── components/
│ ├── auth/
│ │ └── LoginForm.tsx
│ └── __tests__/
│ └── LoginForm.test.tsx
├── hooks/
│ ├── useAuth.ts
│ └── __tests__/
│ └── useAuth.test.ts
├── app/
│ └── api/
│ └── auth/
│ ├── login/
│ │ └── route.ts
│ └── __tests__/
│ └── login.test.ts
└── test/
├── setup.ts
└── utilities.ts
e2e/
└── auth.spec.tsBest Practices
1. Test Behavior, Not Implementation
Good:it('should login user with valid credentials', async () => {
const result = await service.login('test@example.com', 'password123');
expect(result).toHaveProperty('user');
expect(result).toHaveProperty('accessToken');
});it('should call userRepository.findByEmail', async () => {
await service.login('test@example.com', 'password123');
expect(mockUserRepo.findByEmail).toHaveBeenCalled();
// Testing implementation, not behavior
});2. Use Descriptive Test Names
Good:it('should throw error for duplicate email registration', async () => {
// Clear what it tests
});it('should work', async () => {
// Vague, unclear
});3. One Assertion Per Test (Mostly)
Good:it('should validate email format', async () => {
const result = validateEmail('invalid-email');
expect(result).toBe(false);
});
it('should require @ symbol', async () => {
const result = validateEmail('invalidemail.com');
expect(result).toBe(false);
});it('should return user data and tokens on successful login', async () => {
const result = await service.login('test@example.com', 'password123');
expect(result).toHaveProperty('user');
expect(result).toHaveProperty('accessToken');
expect(result).toHaveProperty('refreshToken');
// Related assertions OK in one test
});4. Mock External Dependencies
// Good: Mock database
const mockDb = {
user: {
findUnique: vi.fn(),
create: vi.fn(),
},
};
// Bad: Real database in tests
const result = await db.user.findUnique();
// Slow, unreliable, requires test DB5. Keep Tests Fast
// Good: Unit tests run in milliseconds
it('should validate email', () => {
expect(validateEmail('test@example.com')).toBe(true);
});
// Bad: Slow operations in unit tests
it('should send email', async () => {
await sendEmail('test@example.com'); // Actually sends email!
// Very slow, side effects
});
// Better: Mock it
it('should call email service', async () => {
await sendEmail('test@example.com');
expect(mockEmailService.send).toHaveBeenCalled();
});Next Steps
Feature Development
After testing, continue building features with confidence → Feature Development Guide
Bug Fixing Workflow
Fix bugs with regression tests → Bug Fixing Guide
Quick Reference
Start Testing:/agentful-start
# Specify testing goalnpm testnpm test -- --coveragenpm test -- auth
npm test -- --testNamePattern="login"npm test -- --watch