Testing Front-End Code
Front-end testing is notoriously difficult. UI changes frequently, interactions are complex, and visual correctness is hard to automate. Yet testing is essential—untested front-end code has bugs that reach production, and regression bugs appear when you change unrelated code.
The testing strategy depends on your needs and constraints. Unit tests are fast and cheap. Integration tests are realistic. E2E tests are slow and brittle but cover the full user journey. Visual tests catch unintended visual changes. The right mix depends on your risk tolerance and resources.
Unit Testing Components: React Testing Library
Unit tests verify that a component works correctly. React Testing Library is the modern standard—it encourages testing from the user's perspective, not the implementation.
Rather than testing that a button has a certain CSS class (implementation detail), test that clicking it triggers the expected behaviour (renders, calls a callback, navigates). This is more robust—the component can be refactored without breaking tests.
Example test:
test('button click triggers callback', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalled();
})
Unit tests are fast (milliseconds), easy to write, and excellent for business logic. They're the foundation of the testing pyramid.
Common mistake: testing implementation details. Avoid testing the exact state shape, internal props, or internal function calls. Test user-visible behaviour.
What to Test vs What Not to Test
Test:
- Component renders correct content
- User interactions trigger correct behaviour
- Form validation works
- Error states display
- Conditional rendering works (if permission is false, content doesn't show)
Don't test:
- That a button has a specific CSS class (implementation detail)
- Exact styling (that's what visual testing is for)
- That a library was called (external library tests are the library's job)
- Internal component state (users don't see internal state, they see rendered output)
Visual Regression Testing
Visual regression tests take screenshots and compare them to baseline screenshots. If the screenshot changed, the test fails. This catches unintended visual changes.
Tools: Chromatic (built for Storybook), Percy, BackstopJS, or Playwright's screenshot testing.
Visual testing is powerful for catching "we changed Button styling and accidentally broke ToggleButton" bugs. However, it's not perfect—screenshots can change due to font rendering, anti-aliasing, or timing. Review diffs carefully.
Visual tests are slow (several seconds per test) and shouldn't be your primary test type. But for critical visual areas, they're valuable.
End-to-End Testing: The Full User Journey
E2E tests simulate a real user using the application in a real browser. Open the app, fill a form, submit, verify the result. They test the entire stack: front-end, back-end, database.
Tools: Playwright, Cypress. Both are excellent. Playwright is faster and more reliable, Cypress has better developer experience.
Example E2E test with Playwright:
test('user can sign up', async ({ page }) => {
await page.goto('/signup');
await page.fill('input[name=email]', 'user@example.com');
await page.fill('input[name=password]', 'password123');
await page.click('button:has-text("Sign Up")');
await expect(page).toHaveURL('/dashboard');
})
E2E tests are slow (seconds to minutes per test) and can be brittle (fail if selectors change). Don't over-test with E2E. Focus on critical user journeys: sign up, login, purchase, primary feature.
The Testing Pyramid
The testing pyramid illustrates the ideal test mix:
- Unit tests (base): Many, fast, focus on components and logic
- Integration tests (middle): Moderate number, test components working together
- E2E tests (top): Few, slow, test critical user journeys
An inverted pyramid (many E2E tests, few unit tests) is slow and brittle. Tests fail for integration reasons, and debugging is complex.
Storybook: Developing Components in Isolation
Storybook is a tool for developing, testing, and documenting components in isolation. You write stories (examples) of components and their different states. Storybook renders them in a browser without needing the full application.
Benefits:
- Develop components faster (instant reload, no full app build)
- Test all component states and edge cases easily
- Generate a component library (living documentation)
- Integrate with visual testing tools
- Design and development collaboration (designers can see components)
Storybook is essential for component libraries and design systems. For application-specific components, it's less critical but still valuable.
Test Coverage: The Metric
Test coverage measures how much of your code is executed by tests. 80% coverage means 80% of lines, branches, and functions are tested. Higher coverage is generally better, but more is not always useful.
Focus on coverage for critical code: business logic, error handling, conditionals. Don't obsess over covering trivial code (simple getters, thin rendering functions).
100% coverage is possible but not worth the cost. Some code is tested by integration and E2E tests, and that's fine. The goal is good coverage of code that matters, not perfect coverage of all code.
The Realistic Approach for Most Projects
For most applications:
- Unit tests for business logic and complex components. Test utilities, hooks, form validation. Target 70-80% coverage on the most critical code.
- Integration tests for features. Test a form submission end-to-end with a fake backend. Test that components work together.
- E2E tests for critical user journeys. Sign up, login, payment, primary feature. Maybe 5-10 E2E tests.
- Visual tests for design system components. If you have many shared components, visual regression tests catch unintended changes.
- Manual QA for the rest. Designers and QA people catch issues that automated tests miss.
This balance provides good coverage without excessive testing overhead. You catch critical bugs, prevent regressions on important code, and don't waste time testing trivial code exhaustively.
The Reality of Front-End Testing
Front-end testing requires discipline but dramatically improves code quality and developer confidence. Tests are an investment upfront that pays back immediately when they catch bugs before production, and over time when they enable safe refactoring.
Start with unit tests (fast, valuable, low complexity). Add integration tests as complexity grows. Use E2E tests for critical paths. Avoid the trap of testing implementation details or over-testing trivial code.
Testing is part of good engineering practice. Shipping untested code is gambling—tests are your safety net.