# 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 |