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.

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.