Learn Testing Patterns
56 patterns across 7 categories. Each one shows the convention, a side-by-side example, and why it matters.
Start here
New to testing? Follow these five categories in order.
Unit Testing
Test structure, assertions, naming, arrange-act-assert, and isolation. You'll hit this when tests are hard to read, break on refactors, or test implementation details instead of behavior.
describe("calculateTotal", () => {
it("test1", () => {
expect(calculateTotal([10, 20])).toBe(30);
});
it("test2", () => {
expect(calculateTotal([])).toBe(0);
});
it("test3", () => {
expect(calculateTotal([-5, 5])).toBe(0);
});
});describe("calculateTotal", () => {
it("test1", () => {
expect(calculateTotal([10, 20])).toBe(30);
});
it("test2", () => {
expect(calculateTotal([])).toBe(0);
});
it("test3", () => {
expect(calculateTotal([-5, 5])).toBe(0);
});
});describe("calculateTotal", () => {
it("returns the sum of all items", () => {
expect(calculateTotal([10, 20])).toBe(30);
});
it("returns zero for an empty array", () => {
expect(calculateTotal([])).toBe(0);
});
it("handles negative numbers", () => {
expect(calculateTotal([-5, 5])).toBe(0);
});
});describe("calculateTotal", () => {
it("returns the sum of all items", () => {
expect(calculateTotal([10, 20])).toBe(30);
});
it("returns zero for an empty array", () => {
expect(calculateTotal([])).toBe(0);
});
it("handles negative numbers", () => {
expect(calculateTotal([-5, 5])).toBe(0);
});
});Integration Testing
Database tests, API tests, service boundaries, and end-to-end flows. You'll hit this when unit tests pass but the app breaks in production, or when test setup takes longer than the test itself.
// test/users.integration.test.ts
afterEach(async () => {
await prisma.user.deleteMany();
});
it("creates a user", async () => {
const user = await createUser({ name: "Alice" });
expect(user.name).toBe("Alice");
const count = await prisma.user.count();
expect(count).toBe(1);
});// test/users.integration.test.ts
afterEach(async () => {
await prisma.user.deleteMany();
});
it("creates a user", async () => {
const user = await createUser({ name: "Alice" });
expect(user.name).toBe("Alice");
const count = await prisma.user.count();
expect(count).toBe(1);
});// test/users.integration.test.ts
beforeEach(async () => {
await prisma.comment.deleteMany();
await prisma.post.deleteMany();
await prisma.user.deleteMany();
});
it("creates a user", async () => {
const user = await createUser({ name: "Alice" });
expect(user.name).toBe("Alice");
const count = await prisma.user.count();
expect(count).toBe(1);
});// test/users.integration.test.ts
beforeEach(async () => {
await prisma.comment.deleteMany();
await prisma.post.deleteMany();
await prisma.user.deleteMany();
});
it("creates a user", async () => {
const user = await createUser({ name: "Alice" });
expect(user.name).toBe("Alice");
const count = await prisma.user.count();
expect(count).toBe(1);
});Component Testing
React Testing Library, user-event, accessibility queries, and snapshot testing. You'll hit this when tests break on every CSS change, or when you can't tell whether a component actually works for users.
import { render, screen } from "@testing-library/react";
import { LoginForm } from "./LoginForm";
test("renders the submit button", () => {
render(<LoginForm />);
const button = screen.getByTestId(
"login-submit-btn"
);
expect(button).toBeInTheDocument();
});import { render, screen } from "@testing-library/react";
import { LoginForm } from "./LoginForm";
test("renders the submit button", () => {
render(<LoginForm />);
const button = screen.getByTestId(
"login-submit-btn"
);
expect(button).toBeInTheDocument();
});import { render, screen } from "@testing-library/react";
import { LoginForm } from "./LoginForm";
test("renders the submit button", () => {
render(<LoginForm />);
const button = screen.getByRole("button", {
name: /sign in/i,
});
expect(button).toBeInTheDocument();
});import { render, screen } from "@testing-library/react";
import { LoginForm } from "./LoginForm";
test("renders the submit button", () => {
render(<LoginForm />);
const button = screen.getByRole("button", {
name: /sign in/i,
});
expect(button).toBeInTheDocument();
});Test Strategy
Test pyramid, test trophy, coverage goals, and when to skip tests. You'll hit this when you have 1000 tests but still ship bugs, or when test suites take 45 minutes to run.
// coverage config
module.exports = {
coverageThreshold: {
global: {
branches: 100,
functions: 100,
lines: 100,
statements: 100,
},
},
};
// test to hit coverage
test("constructor sets name", () => {
const u = new User("Alice");
expect(u.name).toBe("Alice");
});
// exists only for line coverage
test("toString returns string", () => {
const u = new User("Bob");
u.toString();
});// coverage config
module.exports = {
coverageThreshold: {
global: {
branches: 100,
functions: 100,
lines: 100,
statements: 100,
},
},
};
// test to hit coverage
test("constructor sets name", () => {
const u = new User("Alice");
expect(u.name).toBe("Alice");
});
// exists only for line coverage
test("toString returns string", () => {
const u = new User("Bob");
u.toString();
});// coverage config
module.exports = {
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
// test validates behavior
test("creates order with line items", () => {
const order = createOrder([item1, item2]);
expect(order.total).toBe(30);
expect(order.items).toHaveLength(2);
});
// test covers an edge case
test("rejects order with no items", () => {
expect(() => createOrder([])).toThrow("empty");
});// coverage config
module.exports = {
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
// test validates behavior
test("creates order with line items", () => {
const order = createOrder([item1, item2]);
expect(order.total).toBe(30);
expect(order.items).toHaveLength(2);
});
// test covers an edge case
test("rejects order with no items", () => {
expect(() => createOrder([])).toThrow("empty");
});Mocking & Stubbing
When to mock, mock vs stub vs spy, over-mocking, and dependency injection. You'll hit this when mocked tests pass but production fails, or when changing one module breaks 50 test files.
import { vi, test, expect } from "vitest";
import { getUser } from "./user-service";
import * as db from "./db";
vi.mock("./db");
test("returns formatted user", async () => {
// Re-implement the dependency
vi.mocked(db.findById).mockImplementation(
async (id: number) => {
if (id === 1) {
return { id: 1, name: "Alice", role: "admin" };
}
throw new Error("not found");
}
);
const result = await getUser(1);
expect(result).toEqual({
id: 1,
displayName: "Alice (admin)",
});
});import { vi, test, expect } from "vitest";
import { getUser } from "./user-service";
import * as db from "./db";
vi.mock("./db");
test("returns formatted user", async () => {
// Re-implement the dependency
vi.mocked(db.findById).mockImplementation(
async (id: number) => {
if (id === 1) {
return { id: 1, name: "Alice", role: "admin" };
}
throw new Error("not found");
}
);
const result = await getUser(1);
expect(result).toEqual({
id: 1,
displayName: "Alice (admin)",
});
});import { vi, test, expect } from "vitest";
import { getUser } from "./user-service";
import * as db from "./db";
vi.mock("./db");
test("returns formatted user", async () => {
// Provide the data the function needs
vi.mocked(db.findById).mockResolvedValue({
id: 1,
name: "Alice",
role: "admin",
});
const result = await getUser(1);
expect(result).toEqual({
id: 1,
displayName: "Alice (admin)",
});
});import { vi, test, expect } from "vitest";
import { getUser } from "./user-service";
import * as db from "./db";
vi.mock("./db");
test("returns formatted user", async () => {
// Provide the data the function needs
vi.mocked(db.findById).mockResolvedValue({
id: 1,
name: "Alice",
role: "admin",
});
const result = await getUser(1);
expect(result).toEqual({
id: 1,
displayName: "Alice (admin)",
});
});Async & Timing
Async assertions, timers, flaky tests, race conditions, and waitFor patterns. You'll hit this when tests pass locally but fail in CI, or when you add sleep calls to make tests green.
// Testing an async user fetch
test("loads user data", (done) => {
fetchUser("u-42").then((user) => {
expect(user).toEqual({
id: "u-42",
name: "Alice",
role: "admin",
});
done();
});
});// Testing an async user fetch
test("loads user data", (done) => {
fetchUser("u-42").then((user) => {
expect(user).toEqual({
id: "u-42",
name: "Alice",
role: "admin",
});
done();
});
});// Testing an async user fetch
test("loads user data", async () => {
const user = await fetchUser("u-42");
expect(user).toEqual({
id: "u-42",
name: "Alice",
role: "admin",
});
});// Testing an async user fetch
test("loads user data", async () => {
const user = await fetchUser("u-42");
expect(user).toEqual({
id: "u-42",
name: "Alice",
role: "admin",
});
});CI & Test Infrastructure
Test runners, parallel execution, test environments, reporting, and flake detection. You'll hit this when your CI pipeline takes 30 minutes, tests fight over shared state, or nobody trusts the test suite.
# GitHub Actions workflow
name: Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx vitest --no-threads
- run: npx vitest --project e2e
- run: npx vitest --project integration# GitHub Actions workflow
name: Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx vitest --no-threads
- run: npx vitest --project e2e
- run: npx vitest --project integration# GitHub Actions workflow
name: Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx vitest --reporter=verbose# GitHub Actions workflow
name: Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx vitest --reporter=verbose