Testing Pyramid Guide
The testing pyramid provides a framework for how many tests to write at each level. More unit tests at the base, fewer E2E tests at the top.
The Pyramid
/ E2E \ ~10% — Slow, expensive, high confidence
/----------\
/ Integration \ ~20% — Medium speed, tests boundaries
/----------------\
/ Unit Tests \ ~70% — Fast, cheap, isolated
/____________________\
Unit Tests
What they test: Individual functions, classes, or components in isolation.
Characteristics:
- Fast (milliseconds)
- No external dependencies (database, network, filesystem)
- Mock or stub all dependencies
- Test one behavior per test
// Pure function — ideal for unit testing
function calculateDiscount(price: number, tier: "basic" | "premium"): number {
return tier === "premium" ? price * 0.2 : price * 0.1;
}
describe("calculateDiscount", () => {
it("applies 20% discount for premium tier", () => {
expect(calculateDiscount(100, "premium")).toBe(20);
});
it("applies 10% discount for basic tier", () => {
expect(calculateDiscount(100, "basic")).toBe(10);
});
});
Tools: Vitest, Jest
Integration Tests
What they test: How multiple units work together, including real external services.
Characteristics:
- Medium speed (seconds)
- Use real database, real API calls (test environment)
- Test boundaries between systems
- Verify data flows correctly through layers
describe("POST /api/users", () => {
it("creates a user and returns 201", async () => {
const res = await request(app)
.post("/api/users")
.send({ name: "Alice", email: "alice@test.com" });
expect(res.status).toBe(201);
expect(res.body.user.email).toBe("alice@test.com");
// Verify it actually hit the database
const user = await db.user.findUnique({ where: { email: "alice@test.com" } });
expect(user).not.toBeNull();
});
});
Tools: Vitest + Supertest, Playwright API testing
End-to-End (E2E) Tests
What they test: Complete user flows through the real application.
Characteristics:
- Slow (seconds to minutes)
- Run against a deployed or locally running app
- Test critical user journeys
- Most expensive to maintain
test("user can sign up and create a post", async ({ page }) => {
await page.goto("/signup");
await page.fill("[name=email]", "test@example.com");
await page.fill("[name=password]", "secure123");
await page.click("button[type=submit]");
await expect(page).toHaveURL("/dashboard");
await page.click("text=New Post");
await page.fill("[name=title]", "My First Post");
await page.click("text=Publish");
await expect(page.locator(".success-toast")).toBeVisible();
});
Tools: Playwright, Cypress
Cost/Benefit Analysis
| Level |
Speed |
Maintenance |
Confidence |
Coverage |
| Unit |
Fast |
Low |
Logic correctness |
Narrow |
| Integration |
Medium |
Medium |
System boundaries |
Medium |
| E2E |
Slow |
High |
User experience |
Broad |
What NOT to Test
- Third-party library internals
- Framework behavior (Next.js routing works)
- Implementation details (internal state shape)
- One-off scripts or throwaway code