2026-05-07 22:06:00 +02:00
|
|
|
# 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
|
2026-05-07 22:08:00 +02:00
|
|
|
it("describes expected behavior", () => {
|
2026-05-07 22:06:00 +02:00
|
|
|
// Arrange: Set up test data and preconditions
|
2026-05-07 22:08:00 +02:00
|
|
|
const input = { title: "Test Task", priority: "high" };
|
2026-05-07 22:06:00 +02:00
|
|
|
|
|
|
|
|
// Act: Perform the action being tested
|
|
|
|
|
const result = createTask(input);
|
|
|
|
|
|
|
|
|
|
// Assert: Verify the outcome
|
2026-05-07 22:08:00 +02:00
|
|
|
expect(result.title).toBe("Test Task");
|
|
|
|
|
expect(result.priority).toBe("high");
|
|
|
|
|
expect(result.status).toBe("pending");
|
2026-05-07 22:06:00 +02:00
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Test Naming Conventions
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Pattern: [unit] [expected behavior] [condition]
|
2026-05-07 22:08:00 +02:00
|
|
|
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", () => {});
|
2026-05-07 22:06:00 +02:00
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Common Assertions
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Equality
|
2026-05-07 22:08:00 +02:00
|
|
|
expect(result).toBe(expected); // Strict equality (===)
|
|
|
|
|
expect(result).toEqual(expected); // Deep equality (objects/arrays)
|
|
|
|
|
expect(result).toStrictEqual(expected); // Deep equality + type matching
|
2026-05-07 22:06:00 +02:00
|
|
|
|
|
|
|
|
// 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);
|
2026-05-07 22:08:00 +02:00
|
|
|
expect(result).toBeCloseTo(0.3, 5); // Floating point
|
2026-05-07 22:06:00 +02:00
|
|
|
|
|
|
|
|
// Strings
|
|
|
|
|
expect(result).toMatch(/pattern/);
|
2026-05-07 22:08:00 +02:00
|
|
|
expect(result).toContain("substring");
|
2026-05-07 22:06:00 +02:00
|
|
|
|
|
|
|
|
// Arrays / Objects
|
|
|
|
|
expect(array).toContain(item);
|
|
|
|
|
expect(array).toHaveLength(3);
|
2026-05-07 22:08:00 +02:00
|
|
|
expect(object).toHaveProperty("key", "value");
|
2026-05-07 22:06:00 +02:00
|
|
|
|
|
|
|
|
// Errors
|
|
|
|
|
expect(() => fn()).toThrow();
|
|
|
|
|
expect(() => fn()).toThrow(ValidationError);
|
2026-05-07 22:08:00 +02:00
|
|
|
expect(() => fn()).toThrow("specific message");
|
2026-05-07 22:06:00 +02:00
|
|
|
|
|
|
|
|
// 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);
|
2026-05-07 22:08:00 +02:00
|
|
|
mockFn.mockResolvedValue({ data: "test" });
|
2026-05-07 22:06:00 +02:00
|
|
|
mockFn.mockImplementation((x) => x * 2);
|
|
|
|
|
|
|
|
|
|
expect(mockFn).toHaveBeenCalled();
|
2026-05-07 22:08:00 +02:00
|
|
|
expect(mockFn).toHaveBeenCalledWith("arg1", "arg2");
|
2026-05-07 22:06:00 +02:00
|
|
|
expect(mockFn).toHaveBeenCalledTimes(3);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Mock Modules
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Mock an entire module
|
2026-05-07 22:08:00 +02:00
|
|
|
jest.mock("./database", () => ({
|
|
|
|
|
query: jest.fn().mockResolvedValue([{ id: 1, title: "Test" }]),
|
2026-05-07 22:06:00 +02:00
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// Mock specific exports
|
2026-05-07 22:08:00 +02:00
|
|
|
jest.mock("./utils", () => ({
|
|
|
|
|
...jest.requireActual("./utils"),
|
|
|
|
|
generateId: jest.fn().mockReturnValue("test-id"),
|
2026-05-07 22:06:00 +02:00
|
|
|
}));
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 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
|
2026-05-07 22:08:00 +02:00
|
|
|
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
2026-05-07 22:06:00 +02:00
|
|
|
|
2026-05-07 22:08:00 +02:00
|
|
|
describe("TaskForm", () => {
|
|
|
|
|
it("submits the form with entered data", async () => {
|
2026-05-07 22:06:00 +02:00
|
|
|
const onSubmit = jest.fn();
|
|
|
|
|
render(<TaskForm onSubmit={onSubmit} />);
|
|
|
|
|
|
|
|
|
|
// Find elements by accessible role/label (not test IDs)
|
2026-05-07 22:08:00 +02:00
|
|
|
await screen.findByRole("textbox", { name: /title/i });
|
|
|
|
|
fireEvent.change(screen.getByRole("textbox", { name: /title/i }), {
|
|
|
|
|
target: { value: "New Task" },
|
2026-05-07 22:06:00 +02:00
|
|
|
});
|
2026-05-07 22:08:00 +02:00
|
|
|
fireEvent.click(screen.getByRole("button", { name: /create/i }));
|
2026-05-07 22:06:00 +02:00
|
|
|
|
|
|
|
|
await waitFor(() => {
|
2026-05-07 22:08:00 +02:00
|
|
|
expect(onSubmit).toHaveBeenCalledWith({ title: "New Task" });
|
2026-05-07 22:06:00 +02:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-07 22:08:00 +02:00
|
|
|
it("shows validation error for empty title", async () => {
|
2026-05-07 22:06:00 +02:00
|
|
|
render(<TaskForm onSubmit={jest.fn()} />);
|
|
|
|
|
|
2026-05-07 22:08:00 +02:00
|
|
|
fireEvent.click(screen.getByRole("button", { name: /create/i }));
|
2026-05-07 22:06:00 +02:00
|
|
|
|
|
|
|
|
expect(await screen.findByText(/title is required/i)).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## API / Integration Testing
|
|
|
|
|
|
|
|
|
|
```typescript
|
2026-05-07 22:08:00 +02:00
|
|
|
import request from "supertest";
|
|
|
|
|
import { app } from "../src/app";
|
2026-05-07 22:06:00 +02:00
|
|
|
|
2026-05-07 22:08:00 +02:00
|
|
|
describe("POST /api/tasks", () => {
|
|
|
|
|
it("creates a task and returns 201", async () => {
|
2026-05-07 22:06:00 +02:00
|
|
|
const response = await request(app)
|
2026-05-07 22:08:00 +02:00
|
|
|
.post("/api/tasks")
|
|
|
|
|
.send({ title: "Test Task" })
|
|
|
|
|
.set("Authorization", `Bearer ${testToken}`)
|
2026-05-07 22:06:00 +02:00
|
|
|
.expect(201);
|
|
|
|
|
|
|
|
|
|
expect(response.body).toMatchObject({
|
|
|
|
|
id: expect.any(String),
|
2026-05-07 22:08:00 +02:00
|
|
|
title: "Test Task",
|
|
|
|
|
status: "pending",
|
2026-05-07 22:06:00 +02:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-07 22:08:00 +02:00
|
|
|
it("returns 422 for invalid input", async () => {
|
2026-05-07 22:06:00 +02:00
|
|
|
const response = await request(app)
|
2026-05-07 22:08:00 +02:00
|
|
|
.post("/api/tasks")
|
|
|
|
|
.send({ title: "" })
|
|
|
|
|
.set("Authorization", `Bearer ${testToken}`)
|
2026-05-07 22:06:00 +02:00
|
|
|
.expect(422);
|
|
|
|
|
|
2026-05-07 22:08:00 +02:00
|
|
|
expect(response.body.error.code).toBe("VALIDATION_ERROR");
|
2026-05-07 22:06:00 +02:00
|
|
|
});
|
|
|
|
|
|
2026-05-07 22:08:00 +02:00
|
|
|
it("returns 401 without authentication", async () => {
|
|
|
|
|
await request(app).post("/api/tasks").send({ title: "Test" }).expect(401);
|
2026-05-07 22:06:00 +02:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## E2E Testing (Playwright)
|
|
|
|
|
|
|
|
|
|
```typescript
|
2026-05-07 22:08:00 +02:00
|
|
|
import { test, expect } from "@playwright/test";
|
2026-05-07 22:06:00 +02:00
|
|
|
|
2026-05-07 22:08:00 +02:00
|
|
|
test("user can create and complete a task", async ({ page }) => {
|
2026-05-07 22:06:00 +02:00
|
|
|
// Navigate and authenticate
|
2026-05-07 22:08:00 +02:00
|
|
|
await page.goto("/");
|
|
|
|
|
await page.fill('[name="email"]', "test@example.com");
|
|
|
|
|
await page.fill('[name="password"]', "testpass123");
|
2026-05-07 22:06:00 +02:00
|
|
|
await page.click('button:has-text("Log in")');
|
|
|
|
|
|
|
|
|
|
// Create a task
|
|
|
|
|
await page.click('button:has-text("New Task")');
|
2026-05-07 22:08:00 +02:00
|
|
|
await page.fill('[name="title"]', "Buy groceries");
|
2026-05-07 22:06:00 +02:00
|
|
|
await page.click('button:has-text("Create")');
|
|
|
|
|
|
|
|
|
|
// Verify task appears
|
2026-05-07 22:08:00 +02:00
|
|
|
await expect(page.locator("text=Buy groceries")).toBeVisible();
|
2026-05-07 22:06:00 +02:00
|
|
|
|
|
|
|
|
// Complete the task
|
|
|
|
|
await page.click('[aria-label="Complete Buy groceries"]');
|
2026-05-07 22:08:00 +02:00
|
|
|
await expect(page.locator("text=Buy groceries")).toHaveCSS(
|
|
|
|
|
"text-decoration-line",
|
|
|
|
|
"line-through",
|
2026-05-07 22:06:00 +02:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Test Anti-Patterns
|
|
|
|
|
|
2026-05-07 22:08:00 +02:00
|
|
|
| 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 |
|