| name | testing |
| description | Testing patterns for reliable code including unit tests, integration tests, and TDD. Use when writing tests, setting up test infrastructure, or improving test coverage. Triggers on test, spec, mock, stub, fixture, coverage, TDD, unit test, integration test, vitest, jest. |
Testing Patterns
Testing strategies and patterns for building reliable, maintainable code with comprehensive test coverage.
When to Use This Skill
- Writing new tests
- Setting up test infrastructure
- Debugging failing tests
- Improving test coverage
- Following TDD methodology
- Writing integration tests
Test Structure
AAA Pattern (Arrange, Act, Assert)
import { describe, it, expect, beforeEach } from 'vitest';
describe('TaskService', () => {
let service: TaskService;
let mockDb: MockDatabase;
beforeEach(() => {
mockDb = createMockDatabase();
service = new TaskService(mockDb);
});
it('should create a task with default priority', async () => {
const outcomeId = 'outcome-123';
const title = 'Implement feature';
const task = await service.createTask({ outcomeId, title });
expect(task.id).toBeDefined();
expect(task.title).toBe(title);
expect(task.priority).toBe(3);
expect(task.status).toBe('pending');
});
it('should throw when outcome does not exist', async () => {
const invalidOutcomeId = 'nonexistent';
await expect(
service.createTask({ outcomeId: invalidOutcomeId, title: 'Test' })
).rejects.toThrow('Outcome not found');
});
});
Test Naming Convention
describe('SessionManager', () => {
it('should start session when outcome has no blockers', () => {});
it('should throw SessionActiveError when session already running', () => {});
it('should emit progress events during task execution', () => {});
it('should cleanup resources when session is stopped', () => {});
it('test start', () => {});
it('calls processTask', () => {});
it('works correctly', () => {});
});
Test Doubles
Mocks, Stubs, and Spies
import { vi, describe, it, expect } from 'vitest';
const stubDatabase = {
getTasks: vi.fn().mockResolvedValue([
{ id: '1', title: 'Task 1', status: 'pending' },
{ id: '2', title: 'Task 2', status: 'completed' },
]),
};
const mockNotificationService = {
send: vi.fn(),
};
const realService = new TaskService();
const spy = vi.spyOn(realService, 'processTask');
describe('using test doubles', () => {
it('should use stub for data', async () => {
const service = new TaskListService(stubDatabase);
const tasks = await service.getPendingTasks();
expect(tasks).toHaveLength(1);
expect(tasks[0].status).toBe('pending');
});
it('should verify mock was called correctly', async () => {
const service = new TaskService(stubDatabase, mockNotificationService);
await service.completeTask('1');
expect(mockNotificationService.send).toHaveBeenCalledWith({
type: 'task_completed',
taskId: '1',
});
expect(mockNotificationService.send).toHaveBeenCalledTimes(1);
});
it('should spy on real implementation', async () => {
await realService.runBatch(['1', '2', '3']);
expect(spy).toHaveBeenCalledTimes(3);
expect(spy).toHaveBeenNthCalledWith(1, '1');
});
});
Creating Mock Factories
import { Session, SessionStatus } from '../../src/types';
interface SessionOverrides {
id?: string;
status?: SessionStatus;
startedAt?: Date;
tasks?: Task[];
}
export function createMockSession(overrides: SessionOverrides = {}): Session {
return {
id: overrides.id ?? `session-${Math.random().toString(36).slice(2)}`,
status: overrides.status ?? 'active',
startedAt: overrides.startedAt ?? new Date(),
tasks: overrides.tasks ?? [],
...overrides,
};
}
const session = createMockSession({ status: 'paused' });
const completedSession = createMockSession({
status: 'completed',
tasks: [createMockTask({ status: 'completed' })],
});
Testing Async Code
Promises and Async/Await
describe('async operations', () => {
it('should fetch user data', async () => {
const user = await userService.getUser('123');
expect(user.name).toBe('Alice');
});
it('should resolve with user', async () => {
await expect(userService.getUser('123')).resolves.toMatchObject({
name: 'Alice',
});
});
it('should reject when user not found', async () => {
await expect(userService.getUser('invalid')).rejects.toThrow('Not found');
});
it('broken test - no await', () => {
expect(userService.getUser('123')).resolves.toBe('Alice');
});
});
Testing Event Emitters
describe('SessionManager events', () => {
it('should emit progress events', async () => {
const manager = new SessionManager();
const events: ProgressEvent[] = [];
manager.on('progress', (event) => events.push(event));
await manager.startSession('outcome-1');
expect(events).toContainEqual(
expect.objectContaining({ type: 'session_started' })
);
});
it('should emit completion event', async () => {
const manager = new SessionManager();
const completionPromise = new Promise<void>((resolve) => {
manager.once('completed', resolve);
});
await manager.startSession('outcome-1');
await completionPromise;
expect(manager.status).toBe('completed');
});
});
Timers and Delays
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
describe('debounced search', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should debounce search calls', async () => {
const searchFn = vi.fn();
const debouncedSearch = debounce(searchFn, 300);
debouncedSearch('a');
debouncedSearch('ab');
debouncedSearch('abc');
await vi.advanceTimersByTimeAsync(300);
expect(searchFn).toHaveBeenCalledTimes(1);
expect(searchFn).toHaveBeenCalledWith('abc');
});
});
Component Testing (React)
Testing Components with Testing Library
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TaskCard } from './TaskCard';
describe('TaskCard', () => {
const defaultProps = {
task: createMockTask({ title: 'Implement feature' }),
onComplete: vi.fn(),
onDelete: vi.fn(),
};
it('should display task title', () => {
render(<TaskCard {...defaultProps} />);
expect(screen.getByText('Implement feature')).toBeInTheDocument();
});
it('should call onComplete when complete button clicked', async () => {
const user = userEvent.setup();
render(<TaskCard {...defaultProps} />);
await user.click(screen.getByRole('button', { name: /complete/i }));
expect(defaultProps.onComplete).toHaveBeenCalledWith(defaultProps.task.id);
});
it('should show confirmation dialog before delete', async () => {
const user = userEvent.setup();
render(<TaskCard {...defaultProps} />);
await user.click(screen.getByRole('button', { name: /delete/i }));
expect(screen.getByText(/are you sure/i)).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /confirm/i }));
expect(defaultProps.onDelete).toHaveBeenCalledWith(defaultProps.task.id);
});
});
Testing Hooks
import { renderHook, act, waitFor } from '@testing-library/react';
import { useSessionState } from './useSessionState';
describe('useSessionState', () => {
it('should initialize with no active session', () => {
const { result } = renderHook(() => useSessionState());
expect(result.current.session).toBeNull();
expect(result.current.isActive).toBe(false);
});
it('should start session and update state', async () => {
const { result } = renderHook(() => useSessionState());
act(() => {
result.current.startSession('outcome-1');
});
await waitFor(() => {
expect(result.current.isActive).toBe(true);
});
expect(result.current.session?.outcomeId).toBe('outcome-1');
});
});
TDD Workflow
Red-Green-Refactor Cycle
describe('calculatePriority', () => {
it('should return high priority for overdue tasks', () => {
const task = createMockTask({
dueDate: new Date('2024-01-01'),
});
expect(calculatePriority(task)).toBe('high');
});
});
function calculatePriority(task: Task): Priority {
if (task.dueDate && task.dueDate < new Date()) {
return 'high';
}
return 'medium';
}
function calculatePriority(task: Task): Priority {
const isOverdue = task.dueDate && isPast(task.dueDate);
const isBlocked = task.blockers.length > 0;
if (isOverdue) return 'high';
if (isBlocked) return 'low';
return 'medium';
}
TDD Benefits
- Tests document intended behavior
- Drives modular, testable design
- Catches regressions immediately
- Builds confidence for refactoring
Integration Testing
Database Integration Tests
import { describe, it, expect, beforeEach, afterAll } from 'vitest';
import { db, resetDatabase } from '../test/database';
describe('TaskRepository integration', () => {
beforeEach(async () => {
await resetDatabase();
});
afterAll(async () => {
await db.close();
});
it('should persist and retrieve tasks', async () => {
const repo = new TaskRepository(db);
const created = await repo.create({
outcomeId: 'outcome-1',
title: 'Test task',
});
expect(created.id).toBeDefined();
const found = await repo.findById(created.id);
expect(found?.title).toBe('Test task');
});
it('should handle concurrent updates', async () => {
const repo = new TaskRepository(db);
const task = await repo.create({ outcomeId: 'o1', title: 'Task' });
await Promise.all([
repo.update(task.id, { priority: 1 }),
repo.update(task.id, { priority: 2 }),
]);
const final = await repo.findById(task.id);
expect([1, 2]).toContain(final?.priority);
});
});
IPC Integration Tests
describe('IPC integration', () => {
it('should handle session start via IPC', async () => {
const handlers = setupIpcHandlers(sessionService);
const result = await handlers.handle('session:start', {
outcomeId: 'outcome-1',
});
expect(result.success).toBe(true);
expect(result.sessionId).toBeDefined();
});
});
Test Organization
File Structure
src/
services/
task-service.ts
task-service.test.ts # Unit tests next to source
test/
setup.ts # Global test setup
factories/ # Mock data factories
session.ts
task.ts
fixtures/ # Static test data
sample-outcome.json
integration/ # Integration tests
database.test.ts
ipc.test.ts
e2e/ # End-to-end tests
session-flow.test.ts
Test Configuration (vitest.config.ts)
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['src/**/*.test.ts', 'test/**/*.test.ts'],
coverage: {
reporter: ['text', 'html'],
exclude: ['test/**', '**/*.d.ts'],
},
setupFiles: ['./test/setup.ts'],
},
});
Testing Checklist
What to Test
What NOT to Test
- Implementation details (private methods)
- Third-party library internals
- Trivial code (simple getters/setters)
- Framework code (React rendering, database driver)
Test Quality Indicators
| Good Test | Bad Test |
|---|
| Tests behavior | Tests implementation |
| Clear failure message | Vague "assertion failed" |
| Single concept | Multiple unrelated assertions |
| Fast execution | Slow, flaky |
| Independent | Depends on test order |
| Readable | Requires deep understanding |