Testing is easy to get wrong. Too many unit tests give false confidence. Too few integration tests miss real bugs. Too many E2E tests make CI slow. Here's a practical guide to the Testing Trophy — the modern testing strategy that actually works.

Testing Strategies for Web Apps: Unit, Integration, E2E, and When to Use Each

The Testing Trophy (Not the Testing Pyramid)

The classic testing pyramid said "lots of unit, some integration, few E2E." The Testing Trophy inverts this: integration tests provide the most confidence per dollar, so write more of them.

Unit TestsIntegration TestsE2E Tests
TestsSingle function/componentMultiple modules togetherFull user flow in browser
SpeedFastest (ms)Fast (10-100ms)Slow (seconds)
ConfidenceLow (isolated)High (integration is the risk)Highest (real UX)
FlakinessNoneLowHigh (network, timing)
DebuggingEasiestModerateHardest
Recommended ratio20%60%20%

Unit Tests — Test Pure Logic Exhaustively

Unit tests shine for pure functions: validation logic, data transformation, utility functions, and business rules. Don't unit test React components in isolation — that's what integration tests are for. Don't test implementation details (test behavior, not methods).

// Good unit test: pure business logic
describe("calculateDiscount", () => {
  it("gives 20% off orders over $100", () => {
    expect(calculateDiscount({ total: 150, coupon: null })).toBe(30);
  });
  it("stacks with coupon, max 50%", () => {
    expect(calculateDiscount({ total: 100, coupon: "SAVE30" })).toBe(40);
  });
});

Integration Tests — The Confidence Backbone

Integration tests verify that multiple units work together. For frontend: render a component with real state, click something, assert the DOM. For backend: hit an endpoint, verify the database state. These catch the bugs unit tests miss.

// Frontend integration test: render + interact + assert
test("submits form and shows success", async () => {
  render(<SignupForm />);
  await user.type(screen.getByLabel("Email"), "test@example.com");
  await user.click(screen.getByText("Sign Up"));
  expect(await screen.findByText("Check your email")).toBeVisible();
});

// Backend integration test: request → response
test("POST /api/users creates user in DB", async () => {
  const res = await request(app)
    .post("/api/users")
    .send({ email: "test@example.com", name: "Test" });
  expect(res.status).toBe(201);
  const user = await db.query("SELECT * FROM users WHERE email = $1", ["test@example.com"]);
  expect(user.rows[0].name).toBe("Test");
});

E2E Tests — Validate Critical User Flows

E2E tests drive a real browser through your most important flows: signup, login, purchase, onboarding. Keep these to critical paths only — they're slow and can be flaky. Playwright is the best E2E tool in 2026.

// E2E: only critical paths
test("user can complete purchase", async ({ page }) => {
  await page.goto("/products/widget");
  await page.click("text=Add to Cart");
  await page.click("text=Checkout");
  await page.fill("[name=card]", "4242424242424242");
  await page.click("text=Pay $29.00");
  await expect(page.locator(".confirmation")).toContainText("Thank you");
});

Testing Stack Recommendations

LayerToolWhen
UnitVitestPure functions, utils, business logic
Component IntegrationVitest + Testing LibraryAny component with user interaction
Backend IntegrationVitest + SupertestAPI endpoints, DB writes
E2EPlaywrightSignup, login, purchase, onboarding
Visual RegressionChromatic / PercyDesign system components

Bottom line: Write mostly integration tests. They provide the best confidence-to-effort ratio. Unit test pure logic. E2E test only critical flows (max 20 scenarios). A slow CI pipeline is a broken one — keep E2E count low. See also: build tools (Vitest is built on Vite) and CI/CD tools comparison.