The generic testing strategy — pyramid, FIRST, test doubles — applies to frontend without modification. What changes is where the layers fall in a frontend codebase and which problems each layer actually catches.
/\ / \ E2E (browser / device) /----\ Few. Critical user journeys. / \ Playwright, Cypress, Detox, Maestro. /--------\ / \ Component / integration / \ Most of the suite for UI work. / \ RTL, Flutter widget test, RN Testing Library. / \ Renders the component with realistic state and /------------------\ asserts user-visible output. / \ / \ Unit / \ Pure logic, hooks, formatters, reducers, selectors. /__________________________\Vitest, Jest, flutter test.
A common misconception in frontend: "the pyramid says lots of unit tests." For UI-heavy code, the bulk often falls in the component layer, not unit — because most of the value of a UI component is in how it renders with state and reacts to user input, neither of which a unit test of an internal function can prove.
The shape that works in practice for a typical product frontend:
~10% E2E — a dozen tests covering the journeys that, if broken, mean revenue is broken.
~70% component / integration — every screen and every reusable component has tests describing what users see.
~20% unit — pure logic, hooks with non-trivial internals, formatters, validators, reducers.
If the unit number is much higher, look hard at whether the tests are exercising real behavior or just internal helpers.
Wrong API endpoint, wrong header, wrong serialization
E2E (or contract test)
Auth flow broken end-to-end
E2E
Layout broken on small screens
Visual regression / manual / responsive test
Animation jank, dropped frames
Performance profiling (not unit)
Memory leak on long sessions
Long-running E2E or profiling
Platform-specific (iOS vs Android, Safari vs Chrome) bug
Cross-browser / cross-device E2E
Notice that several common defects — layout regressions, animation issues, memory leaks — are not well caught by any tier. They need different tools (visual regression, profilers, observability). Tests are necessary but not sufficient.
Frontend has its own version of "tests that test the mock":
Snapshot tests as the entire UI suite. A wall of snapshots that no one diffs is documentation, not testing. Use snapshots for output that is otherwise hard to assert (large rendered structures, generated SQL), not as a substitute for behavioral assertions.
Tests that select by CSS class or test-id everywhere. If every assertion is getByTestId('foo-button'), the test is coupled to implementation. Prefer accessible queries — getByRole, getByLabelText — which mirror how a user finds elements.
Testing the framework. Asserting that useState updates state, or that Flutter's setState triggers a rebuild, tests the framework. Move on.
// Brittle: depends on absolute timingfireEvent.click(button);await new Promise(r => setTimeout(r, 1000));expect(screen.getByText('Saved')).toBeInTheDocument();// Robust: wait until the assertion can succeedfireEvent.click(button);expect(await screen.findByText('Saved')).toBeInTheDocument();
findBy* queries retry until they succeed or time out; waitFor lets you wrap an arbitrary assertion in the same retry behavior. Use these instead of arbitrary sleeps.
Code that calls setTimeout, setInterval, polling, debouncing — use fake timers (vi.useFakeTimers(), jest.useFakeTimers()) and advance them deterministically. Real timers in tests produce real flakiness.
Mock the data-access function (e.g., your getUser() wrapper) — fastest, but the test bypasses the actual fetch / serialization / error handling. Defects in those layers are not caught.
Mock the transport (MSW for fetch/XHR, Detox for native network, Flutter http.Client override) — slightly slower, but the test exercises the real adapter code. Catches more defects.
Hit a real test server — slowest, real wiring, real flakiness. Reserve for a small number of contract or E2E tests.
The default for component tests is transport-level mocking. MSW for web/RN is the de facto standard; Flutter has http.MockClient and the dio package's MockAdapter.
If a user reported a bug that this code now passes, would at least one of these tests fail?
If yes, the suite is doing its job. If no, the tests assert the implementation, not the user-visible behavior. Add the test that would catch the user's bug.