Testing
Testing is not just about verifying that code works. It’s about ensuring that applications remain reliable, maintainable, and accessible as they evolve. Good tests are resilient to refactoring, mirror real user behavior, and provide confidence without slowing down development.
1. Testing philosophy
Test user behavior, not implementation details
- Focus on what users see and do, not internal component structures.
- Avoid testing state variables, class names, or DOM structure unless absolutely necessary.
Good example (behavior-driven)
// Simulates the user finding a button by its visible name and clicking it.
import { render, screen, fireEvent } from '@testing-library/react';
test('should submit the form when the button is clicked', () => {
render();
const submitButton = screen.getByRole('button', { name: /submit/i });
fireEvent.click(submitButton);
// Assert that the form was submitted
});
Bad example (brittle test)
// This test will break if styles change or the implementation is refactored.
const { container } = render(<MyComponent />);
// This test would fail if you switched from CSS modules to Tailwind.
expect(container.querySelector('.submit-button-style')).toBeInTheDocument();
Write tests that survive refactoring
This is the direct benefit of testing behavior over implementation.
A good test should only fail for two reasons:
- Functionality is intentionally changed. (and the test needs to be updated).
- Functionality is accidentally broken (a regression).
A test should not fail if you refactor the code, like renaming a state variable, switching from a <div> to a <section>, or changing a CSS class name, while keeping the user-facing behavior identical.
Mock at the boundaries, not within the application
- Mock external dependencies (APIs, service workers, network requests).
- Avoid mocking internal application modules or functions. If you mock a function that Component A imports from
utils.js, your test is now coupled to the internal architecture of your app, not its behavior. - A For example
fetchcalls orservice workercalls are such interfaces.
Structure tests with AAA (“Arrange, Act, Assert”)
This pattern (also known as “Given, When, Then”) makes tests drastically easier to read and understand. Every test should have three distinct parts:
- Arrange: Set up the test. Render the component, find the elements, and prepare any mock data.
- Act: Perform the user action you are testing (e.g., click a button, type in a form).
- Assert: Check the outcome. Did the component behave as expected after the action?
test('should show an error message when the form is submitted with an invalid email', () => {
// Arrange
render();
const emailInput = screen.getByLabelText(/email/i);
const submitButton = screen.getByRole('button', { name: /submit/i });
// Act
fireEvent.change(emailInput, { target: { value: 'invalid-email' } });
fireEvent.click(submitButton);
// Assert
const errorMessage = screen.getByText(/please enter a valid email address/i);
expect(errorMessage).toBeInTheDocument();
});
Prioritize accessible queries
The Testing Library query API is organized by accessibility priority. By following this priority, you not only write more robust tests but you also build more accessible applications. If your component is hard to test with accessible queries, it’s likely hard for users with assistive technologies to use.
The Priority Order:
- Queries accessible to everyone:
getByRole,getByLabelText,getByPlaceholderText,getByText,getByDisplayValue. - Semantic Queries:
getByAltText,getByTitle. - Test ID:
getByTestId(Use this as a last resort when you can’t match by role or text).
2. Testing levels
Unit tests (base)
Fast, isolated tests for individual functions, components, or hooks. For UI components, Storybook is recommended. For functions and hooks Vitest is recommended.
Writing stories for UI components is important. It provides a directory of components for easy lookup. The components can also be tested individually with manual or automated means.
- Verify visuals: Quickly see if the component renders correctly across all its states.
- Test interactivity: Use the Controls addon to tweak props in real-time and see how the component responds.
- Check responsiveness: Use the Viewport addon to see how the component looks on different screen sizes (mobile, tablet, desktop).
- Explore edge cases: Manually input weird data or long strings into props to see if the layout breaks.
Integration tests (middle)
- Test how multiple components work together, often involving user interactions within a subset of the application. For example, a form composed of different input elements will be considered a unit test.
- Integration testing are setup up similar to unit testing with the possible exception of setup requirements. The setup requirements may include hydrating initial state and mocking APIs.
End-to-end tests (top)
- Simulate real user flows in a browser (e.g., login > dashboard > logout)..
- Since these tests are expensive, cover only critical paths.
- These kinds of tests are generally done with browser emulation and dev servers and should not require any kind of mocking.
- We recommend using
Playwrightfor running these kinds of tests. Along with the usual Assertion/Snapshot based tests, We also recommend adding visual comparisons to test for any visual regressions.
Playwrightalso provides integrations with axe-core for testing accessibility. For a more comprehensive assessment of Web Content Accessibility Guidelines (WCAG) 2.1 Level AA , axe accessibility testing engine is recommended.- Ensure compliance with WCAG 2.1 AA.
3. Additional best practices
- Test pyramid balance
- More unit tests > fewer integration tests > minimal but critical E2E tests.
- Accessibility testing
- Integrate axe-core in both E2E and Storybook environments.
- CI integration
- Run tests automatically in CI (GitHub Actions, GitLab CI, etc.).
- Fail builds on test failures or accessibility violations.
- Coverage tracking
- Use
vitest --coverage(or Jest equivalent). - Set minimum thresholds (e.g., 80%) but prioritize meaningful coverage over numbers.
- Use
Key takeaways
- Write tests that reflect user behavior, not implementation.
- Structure tests with Arrange, Act, Assert for clarity.
- Mock only at boundaries (APIs, services).
- Balance unit, integration, and E2E tests to optimize speed and reliability.
- Use tools like Playwright + axe-core for end-to-end and accessibility testing.
- Ensure tests are part of CI/CD, preventing regressions before they reach production.
Do check out our handbook on Quality Engineering.







