Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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 ScopeTimeTest Count
Component (single component)5-15 minutes3-8 tests
Service (single service)10-20 minutes8-15 tests
Feature (complete feature)30-60 minutes20-40 tests
Suite (multiple features)1-4 hours50-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 -- --passWithNoTests

2. 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 seconds

Step 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);
  });
});
Output:
🧪 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 minutes

Step 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');
  });
});
Output:
🧪 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 minutes

Step 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();
  });
});
Output:
🧪 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 minutes

Step 5: Coverage Analysis

Agent: @reviewer Time: 2-3 minutes

npm test -- --coverage
Coverage report:
----------|---------|----------|---------|---------|-------------------
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
Fill gaps:
// 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,
  });
});
Final coverage:
----------|---------|----------|---------|---------|
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 runs

Final 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.ts

Best 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');
});
Bad:
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
});
Bad:
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);
});
Acceptable:
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 DB

5. 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 goal
Run All Tests:
npm test
Coverage Report:
npm test -- --coverage
Run Specific Tests:
npm test -- auth
npm test -- --testNamePattern="login"
Watch Mode:
npm test -- --watch