Basic Docs
testingquality

Testing Strategies

Unit, Integration, and E2E testing using the testing pyramid approach.

The Testing Pyramid

The pyramid is a mental model for balancing test types. A wide base of fast unit tests supports fewer, slower integration and E2E tests on top. Inverting the pyramid produces a slow, expensive test suite.

Testing Pyramid
E2E — Fewest, slowest, highest user confidence
Integration — Verify component interactions
Unit — Most, fastest, isolate individual logic
Test Distribution
Unit Tests
Many, fast — isolate individual logic
Integration Tests
Some — verify component interactions
E2E Tests
Few, slow — highest user confidence
iHealthy ratio
Aim for roughly 70% unit, 20% integration, and 10% E2E. Over-indexing on E2E tests leads to slow CI pipelines and flaky builds.

Unit Testing

Unit tests verify one function or module in complete isolation. They are the fastest feedback loop available and should cover both happy paths and edge cases.

pricing.test.ts
import { calculateDiscount } from "./pricing";

describe("calculateDiscount", () => {
  it("applies 10% for premium users", () => {
    expect(calculateDiscount(100, "premium")).toBe(90);
  });

  it("returns full price for standard users", () => {
    expect(calculateDiscount(100, "standard")).toBe(100);
  });

  it("throws on negative price", () => {
    expect(() => calculateDiscount(-1, "premium")).toThrow();
  });
});
Isolated
Test one unit at a time. Mock all external dependencies like databases and APIs.
Fast
Unit tests should complete in milliseconds. No network, disk I/O, or timers.
Deterministic
Same input always produces the same output. Avoid random values or real timestamps.
Descriptive Names
Test names are documentation: `it('returns null when user is inactive')` beats `it('works')`.

Integration Testing

Integration tests verify that multiple components — routes, services, and databases — work correctly together. They catch contract mismatches that unit tests cannot see.

users.integration.test.ts
describe("POST /api/users", () => {
  it("creates a user and returns 201", async () => {
    const response = await request(app)
      .post("/api/users")
      .send({ name: "Alice", email: "alice@example.com" });

    expect(response.status).toBe(201);
    expect(response.body).toMatchObject({ name: "Alice" });
  });

  it("returns 409 when email already exists", async () => {
    await createUser({ email: "alice@example.com" });
    const response = await request(app)
      .post("/api/users")
      .send({ email: "alice@example.com" });

    expect(response.status).toBe(409);
  });
});
Test Runner
App Layer
Service Layer
Database

E2E Testing

E2E tests drive a real browser through complete user flows. They provide the highest confidence that the product works end-to-end, but are the slowest and most brittle layer — use them sparingly for critical paths.

login.e2e.ts
import { expect, test } from "@playwright/test";

test("user can log in and see dashboard", async ({ page }) => {
  await page.goto("/login");
  await page.fill("[name=email]", "user@example.com");
  await page.fill("[name=password]", "password");
  await page.click("[type=submit]");

  await expect(page).toHaveURL("/dashboard");
  await expect(page.getByRole("heading")).toContainText("Welcome");
});
ToolBest For
PlaywrightModern apps, multi-browser, CI-friendly
CypressGreat DX, real-time reload, component testing
SeleniumLegacy support, broad language bindings
WebdriverIOMobile + desktop cross-platform testing

What to Test

Test behavior, not implementation
Test what the code does, not how it does it. Tests coupled to internal details break on every refactor and provide false safety.
70%
Unit Tests
of total test suite
20%
Integration
of total test suite
10%
E2E Tests
critical paths only
~ms
Unit Speed
per test execution
1
Critical Paths First
Start with flows that, if broken, immediately impact users or revenue — login, checkout, core APIs.
2
Edge Cases and Boundaries
Cover null inputs, empty arrays, maximum values, and known failure modes that have caused bugs before.
3
Regression Tests for Every Bug
Every bug fixed should get a test. This prevents the same issue from resurfacing silently in future changes.
Built: 4/8/2026, 12:01:11 PM