Tester Agent
The Tester Agent ensures code quality through comprehensive testing, achieving 80% code coverage across all features.
Overview
The Tester Agent is responsible for all testing activities:
- Unit tests (services, components, utilities)
- Integration tests (API endpoints, module interactions)
- E2E tests (full user flows)
- Test fixtures and mocks
- Coverage reporting and improvement
Configuration
name: tester
description: Writes comprehensive unit, integration, and E2E tests. Ensures coverage meets 80% threshold.
model: sonnet
tools: Read, Write, Edit, Glob, Grep, BashWhy Sonnet? Test writing requires understanding patterns and frameworks but doesn't need the highest reasoning level.
Test Framework Selection
Based on the project's existing setup:
| Framework | Use Case | Examples |
|---|---|---|
| Vitest | Modern Vite projects, fast execution | describe, it, expect, vi |
| Jest | React, Next.js, Node.js, Create React App | describe, test, expect, jest |
| Playwright | E2E browser testing, cross-browser | page, locator, expect |
| Cypress | E2E testing with real browser | cy, commands, custom queries |
| Testing Library | Component testing (React, Vue, etc.) | render, screen, fireEvent |
| Supertest | API endpoint testing | request(app).get('/api') |
Implementation Patterns
1. Unit Tests
Test individual functions, classes, components in isolation.
Service Testing
// src/services/__tests__/user.service.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UserService } from '../user.service';
import { UserRepository } from '../../repositories/user.repository';
describe('UserService', () => {
let service: UserService;
let mockRepo: UserRepository;
beforeEach(() => {
// Create mock repository
mockRepo = {
findByEmail: vi.fn(),
create: vi.fn(),
} as any;
service = new UserService(mockRepo);
});
describe('registerUser', () => {
it('should create a new user with hashed password', async () => {
const input = {
email: 'test@example.com',
password: 'password123',
name: 'Test User',
};
// Setup mock return values
mockRepo.findByEmail = vi.fn().mockResolvedValue(null);
mockRepo.create = vi.fn().mockResolvedValue({
id: '1',
email: input.email,
name: input.name,
});
// Execute
const result = await service.registerUser(input);
// Verify
expect(mockRepo.findByEmail).toHaveBeenCalledWith(input.email);
expect(mockRepo.create).toHaveBeenCalled();
expect(result.email).toBe(input.email);
});
it('should throw error if user already exists', async () => {
const input = {
email: 'existing@example.com',
password: 'password123',
name: 'Test User',
};
mockRepo.findByEmail = vi.fn().mockResolvedValue({ id: '1' });
// Verify error
await expect(service.registerUser(input)).rejects.toThrow('User already exists');
expect(mockRepo.create).not.toHaveBeenCalled();
});
it('should handle database errors gracefully', async () => {
const input = {
email: 'test@example.com',
password: 'password123',
name: 'Test User',
};
mockRepo.findByEmail = vi.fn().mockRejectedValue(new Error('Database connection failed'));
await expect(service.registerUser(input)).rejects.toThrow('Database connection failed');
});
});
});- Mock external dependencies
- Test all code paths (happy path + errors)
- Use descriptive test names
- One assertion per test when possible
- Arrange-Act-Assert pattern
Component Testing
// src/components/__tests__/Button.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from '../Button';
describe('Button', () => {
it('should render children', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('should call onClick when clicked', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('should be disabled when isLoading is true', () => {
render(<Button isLoading>Click me</Button>);
const button = screen.getByRole('button');
expect(button).toBeDisabled();
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('should apply variant classes correctly', () => {
const { rerender } = render(<Button variant="primary">Test</Button>);
expect(screen.getByRole('button')).toHaveClass('bg-blue-600');
rerender(<Button variant="danger">Test</Button>);
expect(screen.getByRole('button')).toHaveClass('bg-red-600');
});
it('should not call onClick when disabled', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick} disabled>Click me</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).not.toHaveBeenCalled();
});
});- Test user behavior, not implementation
- Use Testing Library queries (getBy*, queryBy*)
- Test interactions (click, change, submit)
- Test edge cases (disabled, loading, error)
- Accessibility testing
2. Integration Tests
Test how multiple modules work together.
API Integration Testing
// src/app/api/auth/__tests__/login.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { app } from '../../../app';
import request from 'supertest';
describe('POST /api/auth/login', () => {
let testUserId: string;
beforeAll(async () => {
// Setup: create test user
const res = await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
password: 'password123',
name: 'Test User',
});
testUserId = res.body.id;
});
afterAll(async () => {
// Cleanup: delete test user
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('token');
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');
});
it('should validate required fields', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com' });
expect(res.status).toBe(400);
expect(res.body.error).toContain('password');
});
it('should reject non-existent users', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({
email: 'nonexistent@example.com',
password: 'password123',
});
expect(res.status).toBe(401);
});
});- Test real HTTP requests/responses
- Use test database (not production)
- Setup/teardown test data
- Test status codes, headers, body
- Test validation and error handling
3. E2E Tests
Test complete user flows in a real browser.
// e2e/auth.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Authentication Flow', () => {
test('should register and login a new user', async ({ page }) => {
// Navigate to register
await page.goto('/register');
// Fill registration form
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="password"]', 'password123');
await page.fill('[name="name"]', 'Test User');
await page.click('button[type="submit"]');
// Should redirect to dashboard
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('text=Welcome, Test User')).toBeVisible();
});
test('should show error on invalid login', 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"]');
await expect(page.locator('text=Invalid credentials')).toBeVisible();
await expect(page).toHaveURL('/login');
});
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('button:has-text("Logout")');
// Should redirect to login
await expect(page).toHaveURL('/login');
});
});- Test real user flows
- Use realistic test data
- Test navigation and redirects
- Test error handling
- Make tests deterministic
Coverage Strategy
1. Achieve 80% Coverage
Run tests with coverage:
# Vitest
npm test -- --coverage
# Jest
npm test -- --coverageCoverage thresholds in package.json:
{
"vitest": {
"coverage": {
"provider": "v8",
"reporter": ["text", "json", "html"],
"threshold": {
"lines": 80,
"functions": 80,
"branches": 80,
"statements": 80
}
}
}
}2. Test Priority
- Critical Path Tests - Happy path for core features
- Edge Cases - Null, empty, boundary values
- Error Handling - What happens when things fail
- Integration - How modules work together
3. Coverage Report Analysis
# Generate HTML coverage report
npm test -- --coverage --reporter=html
# Open report
open coverage/index.htmlLook for:
- Red files (low coverage)
- Uncovered branches (if/else)
- Uncovered lines
- Missing edge cases
Test Fixtures
Mock Data
// __tests__/fixtures/user.ts
export const mockUser = {
id: '1',
email: 'test@example.com',
name: 'Test User',
createdAt: new Date('2026-01-01'),
};
export const mockUsers = [
mockUser,
{
id: '2',
email: 'another@example.com',
name: 'Another User',
createdAt: new Date('2026-01-02'),
},
];Test Utilities
// __tests__/utils/test-helpers.ts
import { render } from '@testing-library/react';
import { UserContext } from '@/contexts/UserContext';
export function renderWithUser(ui, user) {
return render(
<UserContext.Provider value={{ user }}>
{ui}
</UserContext.Provider>
);
}
export function waitForLoading() {
return new Promise(resolve => setTimeout(resolve, 100));
}Database Seeding
// __tests__/setup.ts
import { db } from '@/db';
export async function setupTestDatabase() {
// Clean database
await db.user.deleteMany();
// Seed test data
await db.user.create({
data: {
email: 'test@example.com',
password: 'hashed_password',
name: 'Test User',
},
});
}
export async function cleanupTestDatabase() {
await db.user.deleteMany();
}Testing Checklist
For each feature, ensure:
- Unit tests for all services
- Unit tests for all components
- Integration tests for API endpoints
- E2E tests for critical user flows
- Coverage threshold met (80%)
- All edge cases covered
- Error paths tested
- Tests are deterministic (no flakiness)
- Tests run quickly (optimize slow tests)
- Accessibility tested
File Structure
src/
├── services/
│ ├── user.service.ts
│ └── __tests__/
│ └── user.service.test.ts
├── components/
│ ├── Button.tsx
│ └── __tests__/
│ └── Button.test.tsx
├── app/
│ └── api/
│ └── auth/
│ └── __tests__/
│ └── login.test.ts
└── __mocks__/
└── database.ts # Mocked database
e2e/
├── auth.spec.ts
├── dashboard.spec.ts
└── fixtures/
└── test-data.tsRules
ALWAYS
- Mock external dependencies (APIs, databases)
- Clean up test data (beforeAll, afterAll)
- Use descriptive test names ("should X when Y")
- Test error cases, not just happy paths
- Use Testing Library queries (getBy*, queryBy*)
- Make tests deterministic (no random data, timeouts)
- Run tests before committing
NEVER
- Test third-party libraries (trust they work)
- Write flaky tests (avoid timeouts, race conditions)
- Test implementation details (test behavior, not internals)
- Skip error path testing
- Use production databases in tests
- Leave commented-out test code
Best Practices
Test Organization
// Good: Logical grouping
describe('UserService', () => {
describe('registerUser', () => {
it('should create user with valid data');
it('should throw error if user exists');
it('should hash password');
});
describe('authenticateUser', () => {
it('should return user with valid credentials');
it('should throw error with invalid credentials');
});
});
// Bad: Flat structure
describe('UserService', () => {
it('should create user');
it('should throw error');
it('should authenticate');
// ❌ Hard to find related tests
});Mocking
// Good: Mock only what's needed
vi.mock('@/lib/email', () => ({
sendEmail: vi.fn(),
}));
// Bad: Over-mocking
vi.mock('@/lib/*'); // ❌ Everything mockedTest Speed
// Good: Use fake timers
vi.useFakeTimers();
// ... run code with setTimeout
vi.runAllTimers();
// Bad: Real timers
await new Promise(r => setTimeout(r, 5000)); // ❌ Slow testCommon Tasks
Writing Unit Tests
- Identify what to test (function, component)
- Mock dependencies
- Write happy path test
- Write error path tests
- Write edge case tests
- Verify coverage
Writing Integration Tests
- Setup test database/fixtures
- Test API endpoint
- Verify status code, headers, body
- Test validation
- Test error handling
- Cleanup test data
Writing E2E Tests
- Identify user flow
- Create test with realistic data
- Test navigation and interactions
- Verify expected outcomes
- Test error scenarios
- Make test deterministic
After Implementation
Report to orchestrator:
{
"test_files_created": [
"src/services/__tests__/user.service.test.ts",
"src/components/__tests__/Button.test.tsx",
"e2e/auth.spec.ts"
],
"coverage": {
"lines": 85,
"functions": 82,
"branches": 78,
"statements": 85
},
"passing": true,
"failing_tests": [],
"recommendations": [
"Add tests for error boundary component",
"Increase branch coverage in userService"
]
}Troubleshooting
Flaky Tests
Symptom: Tests pass sometimes, fail sometimes
Solutions:- Remove random data generation
- Use fake timers instead of real timeouts
- Ensure proper cleanup between tests
- Avoid race conditions with proper waiting
Slow Tests
Symptom: Test suite takes minutes
Solutions:- Use mocks instead of real HTTP calls
- Use in-memory database for testing
- Parallelize test execution
- Optimize test database operations
Coverage Not Increasing
Symptom: Adding tests but coverage stays same
Solutions:- Check if tests are actually executing the code
- Run tests with coverage flag
- Verify test files match pattern
- Check for conditional compilation
Integration
With Backend
[Backend] → Implements UserService
↓
[Tester] → Writes unit tests for UserService
↓
[Tester] → Writes integration tests for API
↓
[Backend] → Fixes any failing testsWith Frontend
[Frontend] → Creates Button component
↓
[Tester] → Writes component tests
↓
[Tester] → Tests accessibility
↓
[Frontend] → Fixes issuesWith Reviewer
[Tester] → Writes tests
↓
[Reviewer] → Runs tests
↓
[Reviewer] → Checks coverage (must be 80%)
↓
[Tester] → Adds more tests if below threshold