Testing Confidence Ladder
Testing everything at the UI level is slow and brittle, but testing nothing is risky. The answer lies in layers: fast tests catch logic bugs, integration tests verify flows work together, and visual tests catch UI regressions. This guide covers how to build a test suite that provides genuine confidence without taking too long.
The Testing Layers
Each layer catches different bugs at different speeds, and the pyramid principle still holds: lots of fast tests at the bottom, fewer slow tests at the top:
- Component tests: Fast, focused tests for individual UI components with Component Testing.
- Logic tests: Even faster tests for state management, reducers, and hooks with Stateful Logic Testing, requiring no DOM and no rendering since they exercise pure logic.
- Integration tests: Slower tests that verify multiple components work together with Integration Testing, covering routing, data fetching, and shared state.
- Visual tests: Screenshot comparisons with Visual Regression catch UI changes you didn’t intend to make.
- Accessibility tests: Automated checks and manual screen reader testing with Accessibility Testing.
Create Maintainable Test Data
Copying large JSON fixtures into tests creates maintenance burden that discourages testing.
- Use test builders: Create Test Data Builder functions that generate valid test data with defaults. Getting a user becomes as simple as calling
buildUser(), orbuildUser({ role: 'admin' })when you need a specific role. - Keep builders close: Store builders next to the code they support. When data models change, tests break immediately and obviously, which makes the necessary updates clear.
- Provide smart defaults: Builders should work with zero configuration but allow any property to be overridden. This prevents common issues like undefined property errors while keeping tests concise.
Write Effective Component Tests
Component tests verify UI behaviour without launching a browser. Keep them fast and focused.
- Mock minimally: Only mock what you must. Let real context providers run, use real hooks, and keep dependencies real when possible. Over-mocking creates tests that pass while the real application breaks.
- Test user-facing behaviour: Verify what users see and do: whether the button renders, whether clicking it triggers the correct action, whether form validation works. Avoid testing implementation details like state variable names, since those break during refactoring.
- Keep tests fast: Component test suites should run in seconds. If they take minutes, something needs attention. Fast tests get run frequently; slow tests get skipped.
Test Complete User Journeys
Integration tests verify that components work together. They’re slower but catch issues that component tests miss.
- Test critical paths: Use Integration Testing for essential flows: login, checkout, onboarding. Mock API calls with MSW but test real routing, real state management, and real component interactions.
- Be selective: One integration test per critical user journey is usually sufficient. More than that tends to make suites slow and flaky. Tests that fail randomly teach developers to ignore failures, which undermines the entire test suite.
- Verify analytics: Test that tracking events fire correctly. Broken analytics means making decisions without data, and the problem often goes unnoticed until someone asks about missing metrics.
Protect Visual Design and Accessibility
Automated tests verify logic, while visual and accessibility tests verify experience.
- Snapshot key screens: Run Visual Regression tests on design system components and critical pages. Test light and dark themes at different screen sizes to catch accidental CSS changes early.
- Test accessibility automatically: Use Accessibility Testing to verify ARIA attributes, colour contrast, and keyboard navigation. Automated tools catch common issues while manual screen reader testing catches nuances. Both are valuable.
Maintain Your Test Suite
Tests degrade without maintenance. Treat your test suite like production code.
- Track layered coverage: Overall coverage percentages tell you little. Instead, track which critical flows have component tests, integration tests, visual tests, and accessibility tests. Gaps in critical flows represent risks.
- Fix flaky tests immediately: A test that sometimes passes and sometimes fails is worse than no test because it trains developers to ignore failures. Fix flaky tests the same day they appear, or delete them.
- Match tests to workflow: Run fast tests (component, logic) on every commit. Run slower tests (integration, visual) on pull requests. Run comprehensive accessibility audits before releases. Fast feedback loops matter.
Remember: The best test suite is one your team actually runs. Fast, focused, reliable tests get used. Slow, flaky tests get disabled. Build the former.