React Testing

Mind Map Summary

  • Goal: To verify that React components render and behave correctly.
  • The Testing Pyramid
    • Unit Tests (Most numerous): Test a single component in isolation.
      • Tool: Jest + React Testing Library (RTL).
    • Integration Tests: Test how several components work together.
      • Tool: Jest + RTL.
    • End-to-End (E2E) Tests (Least numerous): Test a full user flow in a real browser.
      • Tool: Cypress, Playwright.
  • Key Libraries
    • Jest: The test runner and framework.
      • Provides: The test() and expect() functions, assertions (.toBe(), .toEqual()), and mocking capabilities.
    • React Testing Library (RTL): A library for rendering components and interacting with them in a user-centric way.
      • Philosophy: “The more your tests resemble the way your software is used, the more confidence they can give you.”
      • Core Functions:
        • render(): Renders a component into a virtual DOM.
        • screen: An object with queries to find elements (e.g., screen.getByRole('button')).
        • userEvent: A companion library for simulating user interactions like clicks and typing.

Core Concepts

1. The Testing Philosophy of RTL

React Testing Library is fundamentally different from older testing libraries like Enzyme. Enzyme encouraged you to test the implementation details of a component (e.g., “is the state count equal to 1?”). RTL encourages you to test the behavior from a user’s perspective (e.g., “when the user clicks the button, do they see the number ‘1’ on the screen?”). This leads to more resilient tests that don’t break when you refactor the internal logic of a component.

2. Queries: Finding Elements

RTL provides queries that find elements the way a user would. You should prioritize them in this order:

  1. getByRole: The most accessible way. Finds elements by their ARIA role (e.g., button, navigation, heading).
  2. getByLabelText: For form fields.
  3. getByPlaceholderText: For form fields.
  4. getByText: Finds elements by the text they contain.
  5. getByTestId: A last resort for elements that have no other natural way to be identified. Use data-testid="..." in your HTML.

3. Assertions: Making Checks

Jest provides the expect function for making assertions. You use it to check if the result of an action matches your expectation.

// Example
expect(screen.getByRole('heading')).toHaveTextContent('Hello World');
expect(myFunction(2)).toBe(4);

4. User Interaction

The @testing-library/user-event library provides a way to simulate real user interactions more accurately than RTL’s built-in fireEvent. It handles details like hover states and typing events more realistically.

import userEvent from '@testing-library/user-event';

// Simulate a user typing and clicking
await userEvent.type(screen.getByRole('textbox'), 'Hello');
await userEvent.click(screen.getByRole('button'));

Practice Exercise

Write unit tests for a React component using Jest and React Testing Library. The tests should cover rendering, user interaction (e.g., clicking a button), and state changes.

Answer

Let’s test a simple counter component.

1. The Component (Counter.jsx)

import React, { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>Simple Counter</h1>
      {/* The text content of this paragraph is what we will check */}
      <p>Current count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

2. The Test File (Counter.test.js)

Create React App sets up Jest and RTL for you. Test files should be named *.test.js or *.spec.js.

import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';

// test() or it() are the functions Jest provides for defining a test case.
ddescribe('Counter Component', () => {
  test('it should render the initial state correctly', () => {
    // 1. Render the component
    render(<Counter />);

    // 2. Find elements on the screen
    const heading = screen.getByRole('heading', { name: /simple counter/i });
    const countParagraph = screen.getByText(/current count: 0/i);
    const incrementButton = screen.getByRole('button', { name: /increment/i });

    // 3. Assert that the elements are in the document
    expect(heading).toBeInTheDocument();
    expect(countParagraph).toBeInTheDocument();
    expect(incrementButton).toBeInTheDocument();
  });

  test('it should increment the count when the button is clicked', async () => {
    // ARRANGE: Render the component
    render(<Counter />);

    // ACT: Find the button and simulate a user click
    const incrementButton = screen.getByRole('button', { name: /increment/i });
    await userEvent.click(incrementButton);

    // ASSERT: Check if the document has been updated correctly
    // The count should now be 1.
    const countParagraph = screen.getByText(/current count: 1/i);
    expect(countParagraph).toBeInTheDocument();

    // ACT AGAIN: Click the button a second time
    await userEvent.click(incrementButton);

    // ASSERT AGAIN: Check the new state
    expect(screen.getByText(/current count: 2/i)).toBeInTheDocument();
  });
});

Explanation

  1. describe block: This is a Jest function for grouping related tests together into a test suite.
  2. First Test (Rendering): This test follows the Arrange-Act-Assert pattern, though the “Act” is just the render.
    • Arrange: We call render(<Counter />) from RTL.
    • Act: (Implicit) The component renders.
    • Assert: We use screen.getBy... queries to find the elements we expect to see initially. We then use Jest’s expect(...).toBeInTheDocument() to assert that they were all rendered successfully.
  3. Second Test (Interaction and State Change): This test simulates a user interaction.
    • Arrange: We render the component.
    • Act: We find the “Increment” button and use userEvent.click() to simulate a click.
    • Assert: We then query the document again, this time looking for the text Current count: 1. The fact that we can find this text proves that the click handler worked correctly, the component’s state was updated, and it re-rendered with the new value. We then click and assert again to ensure it continues to work.

This testing style gives us high confidence that the component works correctly from the user’s point of view.