Topics

On this page

Last updated on Dec 4, 2025

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

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:

  1. Functionality is intentionally changed. (and the test needs to be updated).
  2. 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

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:

  1. Arrange: Set up the test. Render the component, find the elements, and prepare any mock data.
  2. Act: Perform the user action you are testing (e.g., click a button, type in a form).
  3. 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:

  1. Queries accessible to everyone: getByRole, getByLabelText, getByPlaceholderText, getByText, getByDisplayValue.
  2. Semantic Queries: getByAltText, getByTitle.
  3. 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.

Integration tests (middle)

End-to-end tests (top)

3. Additional best practices

Key takeaways

Do check out our handbook on Quality Engineering.


Credits

Authored by Sayed Sayed Sayed Taqui Director of Engineering – React , Imran Imran Imran Sayed Senior WordPress Engineer , Ayush Ayush Ayush Nirwal Senior Software Engineer , Amoghavarsha Amoghavarsha Amoghavarsha Kudaligi Senior Software Engineer , Mayank Mayank Mayank Rana Senior Software Engineer