Subject: Half your React tests broke ... here's the fix
Hey there,
A team I advise migrated to the Next.js App Router in January. By February, 47% of their tests were failing. Not because the code was broken ... the app worked fine. The tests were written for a world where every component runs on the client. Server Components changed the contract.
They spent two weeks debating: mock everything, or rewrite the test suite from scratch. The answer was neither.
This Week's Decision
The Situation: You've migrated to Next.js App Router. Half your tests are broken. Testing Library can't render Server Components directly. Your team is paralyzed between mocking away the server boundary and rewriting every test.
The Insight: The mistake is treating Server Components and Client Components as one testing problem. They're two different execution contexts with two different testing strategies. Trying to test them the same way is why suites break.
A three-layer testing model resolves this cleanly:
Layer 1: Unit test business logic as pure functions (60-70% of tests).
Extract logic from components into pure functions. These are the easiest tests to write, the fastest to run, and the most valuable ... they test actual behavior, not rendering mechanics.
// lib/pricing.ts ... pure function, no React dependency
export function calculateProration(
currentPlan: Plan,
newPlan: Plan,
daysRemaining: number
): number {
const dailyDiff = (newPlan.price - currentPlan.price) / 30;
return Math.max(0, dailyDiff * daysRemaining);
}
// lib/pricing.test.ts ... fast, no rendering
test("prorates upgrade correctly", () => {
const current = { price: 49 };
const next = { price: 99 };
expect(calculateProration(current, next, 15)).toBeCloseTo(25);
});
Layer 2: React Testing Library for Client Components (15-20% of tests).
Client Components ('use client') behave exactly like traditional React components. Test them with RTL as usual ... render, interact, assert. No changes needed.
The key: keep Client Components thin. They handle interactivity and state. Business logic lives in Layer 1 functions they call.
// components/PlanSelector.test.tsx
test('shows prorated amount on plan change', async () => {
render(<PlanSelector currentPlan={basicPlan} />);
await userEvent.click(screen.getByText('Pro Plan'));
expect(screen.getByText('$25.00 prorated')).toBeInTheDocument();
});
Layer 3: HTTP-level integration tests for server routes (10-15% of tests).
Server Components render on the server and return HTML. Test them the way you'd test any server endpoint ... make HTTP requests and assert on the response.
// app/dashboard/page.integration.test.ts
test("dashboard renders user metrics", async () => {
const response = await fetch("http://localhost:3000/dashboard", {
headers: { Cookie: `session=${testSession}` },
});
const html = await response.text();
expect(html).toContain("Monthly Revenue");
expect(html).toContain("Active Users");
expect(response.status).toBe(200);
});
One team implementing this model saw their test suite run 40% faster (pure function tests are orders of magnitude quicker than rendering tests) while catching more real bugs ... because they were testing business logic directly instead of fighting React internals.
When to Apply This:
- Any team using Next.js App Router with an existing test suite
- Teams where more than 20% of tests broke during the Server Component migration
- New projects on App Router that need a testing strategy from day one
Worth Your Time
-
Testing Library: Server Components ... Official guidance on testing Server Components with RTL. Short answer: you mostly don't ... you test the client components they render and the data functions they call.
-
Kent C. Dodds: Testing Implementation Details ... The foundational argument for testing behavior over implementation. Applies directly to the Server Component problem: your tests should verify what users see, not where components render.
-
Vercel: Next.js Testing ... Official testing docs for App Router. The section on testing Server Actions with integration tests fills a gap most tutorials skip.
Tool of the Week
Vitest ... If you haven't migrated from Jest, Vitest runs tests 2-5x faster with Vite's module resolution. Compatible with Testing Library, supports TypeScript natively, and handles the module resolution issues that cause Server Component test failures in Jest. The migration typically takes an afternoon.
That's it for this week.
Hit reply if your test suite broke during the App Router migration. Tell me how many tests are failing ... I'll help you triage which ones to fix, which to rewrite, and which to delete. I read every response.
– Alex
P.S. For the complete guide to modern frontend architecture ... including Server Components, state management, and build optimization: Modern Frontend Architecture.