# Testing Patterns Reference Quick reference for common testing patterns across the stack. Use alongside the `test-driven-development` skill. ## Table of Contents - [Test Structure (Arrange-Act-Assert)](#test-structure-arrange-act-assert) - [Test Naming Conventions](#test-naming-conventions) - [Common Assertions](#common-assertions) - [Mocking Patterns](#mocking-patterns) - [React/Component Testing](#reactcomponent-testing) - [API / Integration Testing](#api--integration-testing) - [E2E Testing (Playwright)](#e2e-testing-playwright) - [Test Anti-Patterns](#test-anti-patterns) ## Test Structure (Arrange-Act-Assert) ```typescript it('describes expected behavior', () => { // Arrange: Set up test data and preconditions const input = { title: 'Test Task', priority: 'high' }; // Act: Perform the action being tested const result = createTask(input); // Assert: Verify the outcome expect(result.title).toBe('Test Task'); expect(result.priority).toBe('high'); expect(result.status).toBe('pending'); }); ``` ## Test Naming Conventions ```typescript // Pattern: [unit] [expected behavior] [condition] describe('TaskService.createTask', () => { it('creates a task with default pending status', () => {}); it('throws ValidationError when title is empty', () => {}); it('trims whitespace from title', () => {}); it('generates a unique ID for each task', () => {}); }); ``` ## Common Assertions ```typescript // Equality expect(result).toBe(expected); // Strict equality (===) expect(result).toEqual(expected); // Deep equality (objects/arrays) expect(result).toStrictEqual(expected); // Deep equality + type matching // Truthiness expect(result).toBeTruthy(); expect(result).toBeFalsy(); expect(result).toBeNull(); expect(result).toBeDefined(); expect(result).toBeUndefined(); // Numbers expect(result).toBeGreaterThan(5); expect(result).toBeLessThanOrEqual(10); expect(result).toBeCloseTo(0.3, 5); // Floating point // Strings expect(result).toMatch(/pattern/); expect(result).toContain('substring'); // Arrays / Objects expect(array).toContain(item); expect(array).toHaveLength(3); expect(object).toHaveProperty('key', 'value'); // Errors expect(() => fn()).toThrow(); expect(() => fn()).toThrow(ValidationError); expect(() => fn()).toThrow('specific message'); // Async await expect(asyncFn()).resolves.toBe(value); await expect(asyncFn()).rejects.toThrow(Error); ``` ## Mocking Patterns ### Mock Functions ```typescript const mockFn = jest.fn(); mockFn.mockReturnValue(42); mockFn.mockResolvedValue({ data: 'test' }); mockFn.mockImplementation((x) => x * 2); expect(mockFn).toHaveBeenCalled(); expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2'); expect(mockFn).toHaveBeenCalledTimes(3); ``` ### Mock Modules ```typescript // Mock an entire module jest.mock('./database', () => ({ query: jest.fn().mockResolvedValue([{ id: 1, title: 'Test' }]), })); // Mock specific exports jest.mock('./utils', () => ({ ...jest.requireActual('./utils'), generateId: jest.fn().mockReturnValue('test-id'), })); ``` ### Mock at Boundaries Only ``` Mock these: Don't mock these: ├── Database calls ├── Internal utility functions ├── HTTP requests ├── Business logic ├── File system operations ├── Data transformations ├── External API calls ├── Validation functions └── Time/Date (when needed) └── Pure functions ``` ## React/Component Testing ```tsx import { render, screen, fireEvent, waitFor } from '@testing-library/react'; describe('TaskForm', () => { it('submits the form with entered data', async () => { const onSubmit = jest.fn(); render(); // Find elements by accessible role/label (not test IDs) await screen.findByRole('textbox', { name: /title/i }); fireEvent.change(screen.getByRole('textbox', { name: /title/i }), { target: { value: 'New Task' }, }); fireEvent.click(screen.getByRole('button', { name: /create/i })); await waitFor(() => { expect(onSubmit).toHaveBeenCalledWith({ title: 'New Task' }); }); }); it('shows validation error for empty title', async () => { render(); fireEvent.click(screen.getByRole('button', { name: /create/i })); expect(await screen.findByText(/title is required/i)).toBeInTheDocument(); }); }); ``` ## API / Integration Testing ```typescript import request from 'supertest'; import { app } from '../src/app'; describe('POST /api/tasks', () => { it('creates a task and returns 201', async () => { const response = await request(app) .post('/api/tasks') .send({ title: 'Test Task' }) .set('Authorization', `Bearer ${testToken}`) .expect(201); expect(response.body).toMatchObject({ id: expect.any(String), title: 'Test Task', status: 'pending', }); }); it('returns 422 for invalid input', async () => { const response = await request(app) .post('/api/tasks') .send({ title: '' }) .set('Authorization', `Bearer ${testToken}`) .expect(422); expect(response.body.error.code).toBe('VALIDATION_ERROR'); }); it('returns 401 without authentication', async () => { await request(app) .post('/api/tasks') .send({ title: 'Test' }) .expect(401); }); }); ``` ## E2E Testing (Playwright) ```typescript import { test, expect } from '@playwright/test'; test('user can create and complete a task', async ({ page }) => { // Navigate and authenticate await page.goto('/'); await page.fill('[name="email"]', 'test@example.com'); await page.fill('[name="password"]', 'testpass123'); await page.click('button:has-text("Log in")'); // Create a task await page.click('button:has-text("New Task")'); await page.fill('[name="title"]', 'Buy groceries'); await page.click('button:has-text("Create")'); // Verify task appears await expect(page.locator('text=Buy groceries')).toBeVisible(); // Complete the task await page.click('[aria-label="Complete Buy groceries"]'); await expect(page.locator('text=Buy groceries')).toHaveCSS( 'text-decoration-line', 'line-through' ); }); ``` ## Test Anti-Patterns | Anti-Pattern | Problem | Better Approach | |---|---|---| | Testing implementation details | Breaks on refactor | Test inputs/outputs | | Snapshot everything | No one reviews snapshot diffs | Assert specific values | | Shared mutable state | Tests pollute each other | Setup/teardown per test | | Testing third-party code | Wastes time, not your bug | Mock the boundary | | Skipping tests to pass CI | Hides real bugs | Fix or delete the test | | Using `test.skip` permanently | Dead code | Remove or fix it | | Overly broad assertions | Doesn't catch regressions | Be specific | | No async error handling | Swallowed errors, false passes | Always `await` async tests |