Frontend testing has a reputation for being flaky, slow, and low-value. That reputation is earned — but it’s usually a strategy problem, not a tooling problem.
The Practical Testing Stack
Forget the theoretical pyramid for a moment. Here’s what gives the best return in frontend code:
- Unit tests for pure logic. Utility functions, data transformations, state reducers. Fast, reliable, high-value.
- Component tests for interaction patterns. Render a component, interact with it, assert on the result.
- E2E tests for critical user flows. Login, checkout, the 3-5 paths that break your business if they fail.
Unit Tests: Keep Them Pure
Test functions, not components, when the logic is separable:
// formatPrice.ts
export function formatPrice(cents: number, currency = "USD"): string {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency,
}).format(cents / 100);
}
// formatPrice.test.ts
import { formatPrice } from "./formatPrice";
test("formats cents to dollar string", () => {
expect(formatPrice(1999)).toBe("$19.99");
expect(formatPrice(500, "EUR")).toBe("€5.00");
expect(formatPrice(0)).toBe("$0.00");
});
These tests run in milliseconds, never flake, and catch real bugs.
Component Tests: Test Behavior, Not Implementation
Use Testing Library’s philosophy: test what the user sees and does.
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { SearchBox } from "./SearchBox";
test("filters results as user types", async () => {
const onSearch = vi.fn();
render(<SearchBox onSearch={onSearch} />);
const input = screen.getByRole("searchbox");
await userEvent.type(input, "astro");
expect(onSearch).toHaveBeenLastCalledWith("astro");
});
Don’t test internal state. Don’t assert on CSS classes. Test the contract: given this input, expect this output.
E2E Tests: Less Is More
Write fewer E2E tests, but make them count. Cover the critical paths:
test("user can complete checkout", async ({ page }) => {
await page.goto("/products");
await page.click('[data-testid="product-card"]');
await page.click("text=Add to Cart");
await page.click("text=Checkout");
await page.fill("#email", "test@example.com");
await page.click("text=Place Order");
await expect(page.locator("text=Order Confirmed")).toBeVisible();
});
Five to ten well-chosen E2E tests catch more real bugs than hundreds of brittle ones.
What Not to Test
- Third-party libraries. Don’t test that React renders or that Axios makes HTTP calls.
- Implementation details. Don’t test internal state, private methods, or CSS class names.
- Snapshot tests (mostly). They catch changes, not bugs. They train people to update snapshots without looking.
The goal is confidence that your code works, with the least maintenance burden. Every test you write should justify its existence.