Testing Strategy and the Pyramid for Frontend
TL;DR
Test behavior, not implementation. The traditional pyramid (many unit, fewer integration, very few E2E) still applies, but frontends benefit from a broader middle layer — RTL/Testing-Library integration tests catch most real bugs at a fraction of E2E’s cost. Each tier has a specific purpose; over-investing in one (a thousand unit tests, no E2E) gives false confidence.
Interview Q&A
Q: What are you testing for, exactly?
A: Three goals, in order:
- Catch regressions — tests fail before bugs ship.
- Document behavior — the test names + assertions communicate what the code does.
- Enable refactor — tests survive implementation changes if they’re written against behavior, not internals.
The third is where junior tests fail. A test that asserts “the count state variable equals 3” breaks when you rename or restructure. A test that asserts “the screen shows Count: 3” survives any refactor that doesn’t change behavior.
Q: The testing pyramid for frontend — what tiers?
A:
▲
/ E2E \ few — critical user journeys (Playwright)
/---------\
/ Integration\ many — components together, with MSW (Testing Library)
/-------------\
/ Unit \ some — pure logic, utilities, reducers (Vitest)
/-----------------\
Differences from backend pyramid:
- Frontend integration is cheaper than backend integration — Testing Library runs in milliseconds, no containers or fixtures.
- Frontend “unit” is narrower — most of your code is component-shaped, which is better tested as integration.
- The “testing trophy” (Kent C. Dodds) — wide middle (integration), narrow ends — fits frontends better than the pyramid in many cases.
Q: What goes in each tier?
A:
Unit (Vitest):
- Pure functions, utilities (date formatters, validators, calculators).
- Reducers, state machines.
- Custom hooks where the hook is purely computational.
Integration (Testing Library + MSW):
- Components rendering, responding to user events, calling APIs.
- Forms — fill, submit, see result.
- Routing — click link, navigate, see new page.
- Custom hooks that integrate with React (data fetching hooks, subscription hooks).
E2E (Playwright):
- Critical user journeys (sign up, checkout, primary task).
- Anything cross-tab / multi-page that integration can’t cover.
- Browser-specific behavior verification.
Q: How many of each?
A: No magic number, but a rough guide for a mid-size app:
| Tier | Count for “feature complete” |
|---|---|
| Unit | 50-200 per app (mostly utilities) |
| Integration | 100-500 — one per meaningful screen + key interactions |
| E2E | 5-30 — the 5-30 things that must work |
If you have 5000 unit tests and 3 integration tests, your suite is the wrong shape — most bugs ship in component integration, not utility logic.
Q: What does “test behavior, not implementation” actually mean?
A:
Implementation test (fragile):
it("calls setCount", () => {
const setCount = vi.fn();
vi.spyOn(React, "useState").mockReturnValue([0, setCount]);
render(<Counter />);
fireEvent.click(screen.getByText("+"));
expect(setCount).toHaveBeenCalledWith(1);
});
This breaks the moment you rename setCount or switch to useReducer.
Behavior test (durable):
it("increments the count", async () => {
render(<Counter />);
const user = userEvent.setup();
expect(screen.getByText("Count: 0")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: /increment/i }));
expect(screen.getByText("Count: 1")).toBeInTheDocument();
});
The test exercises the same path a real user takes. Refactor useState → useReducer → Redux → whatever; the test still passes.
Q: What about coverage targets?
A: Coverage is a signal, not a goal. 100% coverage with assertion-free tests is worse than 70% coverage with behavior tests. Reasonable targets:
- Critical paths (auth, payment, data integrity): aim for high (~90%).
- Utilities: high (easy to test, high ROI).
- UI styling / animation: low (test what behavior they enable, not the styles).
- Generated code / vendor: skip.
Diff coverage (was the changed code in this PR tested?) is more useful than total coverage. See ../../backend/05_testing/strategy/04_test_data_and_ci_parallelization.md.
Q: How do you handle flaky tests?
A: A flaky test is a bug, not a nuisance. Causes:
- Time —
Date.now(),setTimeoutwithout mocking. Fix:vi.useFakeTimers(). - Order dependence — test A leaves state for test B. Fix: reset between tests.
- Race conditions —
expectbefore async work resolved. Fix:await findBy*orwaitFor. - External dependencies — real network calls. Fix: MSW.
- Browser differences (E2E) — different headless modes timing differently.
The cardinal sin: “just re-run it.” Skip flaky tests with .skip and a JIRA ticket; don’t ship a suite that lies about passing.
Q: When does a test belong in a Storybook story instead?
A: Storybook has “play functions” — interactions you can attach to a story:
export const FilledForm: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.type(canvas.getByLabelText(/email/i), "test@test.com");
await userEvent.click(canvas.getByRole("button", { name: /submit/i }));
},
};
Storybook + interactions is great for visual regression of states (filled form, error state, loading state) and for review by designers/product. Pure logic tests stay in Vitest; component states benefit from being visible in Storybook.
Q: Component vs visual vs E2E — when each catches what?
A:
| Bug | Caught by |
|---|---|
| Button click does wrong thing | Component test (RTL) |
| Form validation accepts bad input | Component test |
| Data fetching error not shown | Component test + MSW |
| Color regression after theme refactor | Visual regression (Chromatic) |
| Layout shift on mobile breakpoint | Visual regression at multiple viewports |
| Login flow broken | E2E |
| Real browser bug (Safari-specific) | E2E with browser matrix |
| Accessibility regression | A11y unit test + manual audit |
| Cross-tab consistency | E2E (or manual) |
Component tests catch most bugs. Visual regression catches the ones component tests can’t see. E2E catches the ones that need a real browser context.
Q: How long should the suite take?
A: Targets that protect productivity:
- Local unit + integration: < 30 seconds for the whole suite. Vitest watch mode keeps this < 5s.
- Local on a focused test: < 1 second.
- CI full suite: < 5 minutes (parallelize with sharding).
- E2E suite: < 10 minutes (parallel browsers; consider running on PR vs nightly tiers).
A 40-minute suite gets skipped, gets -x’d, gets .only’d into and breaks. Speed is a feature.
Gotchas / edge cases
renderdoesn’t unmount automatically in old setups — Testing Library auto-cleans between tests in modern setups; verify your config.actwarnings mean an update happened outside React’s batched mode — usually fixed byawait-ing the right thing.- Mocking React itself (
vi.mock("react")) is almost always wrong; you’ve stopped testing React. - Shared mutable state across tests — module-level variables, singletons, global Pinia/Redux stores — reset between tests.
- Date-based assertions without fake timers will fail on certain days/timezones — always mock time.
What a senior is expected to say
- “Test behavior, not implementation. RTL queries by role mirror what a user (and screen reader) sees — refactor-survivable.”
- “The pyramid weights toward integration for frontend. Unit for pure logic, integration for components, E2E for critical journeys only.”
- “Coverage is a signal, not a goal — 100% coverage with no assertions is worse than 70% with behavior tests. Diff coverage is more useful than total.”
- “Flaky tests are bugs. The fix is finding the root cause (time, order, race, external) — never ‘just re-run.’”
- “Speed matters — a 40-minute suite gets skipped. Sharding in CI, watch mode locally.”
Cross-references
- Vitest specifics: 02_vitest_vs_jest.md
- React Testing Library: 03_react_testing_library.md
- E2E: 09_playwright_e2e.md
- Backend testing strategy mirror: ../../backend/05_testing/strategy/
Further reading
- Kent C. Dodds — “Write tests. Not too many. Mostly integration.”: https://kentcdodds.com/blog/write-tests
- Testing Library — Guiding Principles: https://testing-library.com/docs/guiding-principles
- Martin Fowler — “Test Pyramid”: https://martinfowler.com/articles/practical-test-pyramid.html