Jest and React Testing Library-Best Practices for Front-End Testing

Tianya SchoolTianya School
6 min read

Jest and React Testing Library (RTL) are the preferred tools for testing React applications in front-end development. Jest is a feature-rich JavaScript testing framework, while React Testing Library is a library that encourages writing tests from the user's perspective, focusing on testing component behavior rather than internal implementation details.

Installation and Configuration

First, ensure you have installed react, react-dom, jest, @testing-library/react, and @testing-library/jest-dom. Add the following dependencies to your package.json:

npm install --save-dev jest @testing-library/react @testing-library/jest-dom
# or
yarn add --dev jest @testing-library/react @testing-library/jest-dom

Configure Jest in jest.config.js, for example:

module.exports = {
  setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
  testEnvironment: 'jsdom',
};

Basic Test Structure

Create a test file, typically with the same name as your component file but with a .test.js or .test.tsx suffix. Below is an example of a simple component test:

import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import MyComponent from './MyComponent';

describe('MyComponent', () => {
  it('renders correctly', () => {
    render(<MyComponent />);
    expect(screen.getByText('Hello, world!')).toBeInTheDocument();
  });

  it('handles button click', () => {
    render(<MyComponent />);
    const button = screen.getByRole('button', { name: /click me/i });
    fireEvent.click(button);
    expect(screen.getByText(/clicked/i)).toBeInTheDocument();
  });
});

Testing Component Behavior

Use the render function to render the component and the screen object to query the DOM, ensuring the component renders as expected. Helper functions like getByText, getByRole, and getByPlaceholderText can help locate elements.

Mocking

Jest provides powerful mocking capabilities to simulate component dependencies, such as API calls. For example, mocking a fetch call:

import fetch from 'jest-fetch-mock';

beforeAll(() => {
  fetch.mockResponseOnce(JSON.stringify({ data: 'mocked response' }));
});

it('fetches data on mount', async () => {
  render(<MyComponent />);
  await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));
});

Event Handling

Use the fireEvent function to trigger events on components, such as clicking a button or submitting a form:

const button = screen.getByRole('button');
fireEvent.click(button);

Cleanup and Teardown

After each test, ensure you clean up any side effects, such as elements added to the DOM. The afterEach hook can be used for this purpose:

afterEach(() => {
  cleanup();
});

Asynchronous Testing

Use waitFor or async/await to handle asynchronous operations, ensuring the component reaches the expected state during tests:

it('loads data after fetching', async () => {
  render(<MyComponent />);
  await waitFor(() => expect(screen.getByText('Data loaded')).toBeInTheDocument());
});

Testing State and Side Effects

Use jest.useFakeTimers() and the act function to test state changes and side effects, such as timers or effect functions:

jest.useFakeTimers();

it('displays loading state', () => {
  render(<MyComponent />);
  act(() => jest.advanceTimersByTime(1000));
  expect(screen.getByText('Loading...')).toBeInTheDocument();
});

Testing Component Libraries

For complex component libraries, create a setupTests.js file to set up global mocks and configurations, for example:

import '@testing-library/jest-dom';
import fetchMock from 'jest-fetch-mock';

fetchMock.enableMocks(); // If using fetch mocks

Performance Optimization

Using jest-environment-jsdom-sixteen or jest-environment-jsdom-thirteen can reduce memory consumption during tests.

Testing Component Interactivity

React Testing Library emphasizes testing component behavior rather than implementation details. Below are some best practices for testing component interactivity:

Testing User Interactions

Use fireEvent to simulate user behavior, such as clicking, typing, and selecting:

const input = screen.getByLabelText('Search');
fireEvent.change(input, { target: { value: 'search term' } });
expect(input).toHaveValue('search term');

Ensuring Component Response to Changes

Test how components respond to state or props changes:

const toggleButton = screen.getByRole('button', { name: 'Toggle' });
fireEvent.click(toggleButton);
expect(screen.getByTestId('visible-element')).toBeInTheDocument();

Validating Data Rendering

Test whether the component correctly renders data fetched from an API:

const data = { title: 'Example Title' };
fetchMock.mockResponseOnce(JSON.stringify(data));

render(<MyComponent />);
await waitFor(() => expect(screen.getByText('Example Title')).toBeInTheDocument());

Error and Exception Handling

Test the component's behavior when errors occur, such as verifying the display of error messages:

it('displays error message when fetching fails', async () => {
  fetchMock.mockRejectOnce(new Error('Network error'));
  render(<MyComponent />);
  await waitFor(() => expect(screen.getByText('Error: Network error')).toBeInTheDocument());
});

Clear Test Descriptions

Write meaningful test descriptions to make test results easy to understand:

it('renders search results when query is submitted', async () => {
  // ...
});

Testing Edge Cases

Ensure you cover all edge cases for the component, including null values, abnormal data, and boundary conditions:

it('displays loading state when data is fetching', () => {
  render(<MyComponent isLoading />);
  expect(screen.getByText('Loading...')).toBeInTheDocument();
});

it('displays empty state when no data is found', () => {
  render(<MyComponent data={[]} />);
  expect(screen.getByText('No results found.')).toBeInTheDocument();
});

Code Coverage Report

Use the jest-coverage plugin to generate a code coverage report, ensuring sufficient test coverage:

npx jest --coverage

Continuous Integration

Integrate tests into the continuous integration (CI) process to ensure consistent code quality:

# .github/workflows/test.yml (GitHub Actions)
name: Test

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout code
      uses: actions/checkout@v2
    - name: Setup Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '14.x'
    - name: Install dependencies
      run: npm ci
    - name: Run tests
      run: npm test

Advanced Testing Techniques

Mocking and Spying

Jest provides mocking and spying capabilities to control and inspect function behavior:

import myFunction from './myFunction';

jest.spyOn(myModule, 'myFunction');

// Call the function in the test
myFunction();

// Check if the function was called
expect(myFunction).toHaveBeenCalled();

// Check the specific arguments of the function call
expect(myFunction).toHaveBeenCalledWith(expectedArgs);

// Reset the mock
myFunction.mockReset();

// Reset and clear the mock's return value and call history
myFunction.mockClear();

// Restore the original function
myFunction.mockRestore();

Testing Asynchronous Logic

Use async/await and await waitFor to handle asynchronous operations:

it('fetches data and updates state', async () => {
  // Mock API response
  fetchMock.mockResolvedValueOnce({ json: () => Promise.resolve({ data: 'mocked data' }) });

  render(<MyComponent />);

  // Wait for data to load
  await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));

  // Verify state update
  expect(screen.getByText('mocked data')).toBeInTheDocument();
});

Testing Lifecycle Methods

Use act to wrap lifecycle methods, ensuring they execute correctly in the test environment:

import { act } from 'react-dom/test-utils';

it('calls componentDidMount', () => {
  const mockFn = jest.fn();
  const MyComponent = () => {
    useEffect(mockFn);
    return <div>Component</div>;
  };

  act(() => {
    render(<MyComponent />);
  });

  expect(mockFn).toHaveBeenCalled();
});

Using createRef and forwardRef

When testing components that use createRef or forwardRef, create a ref and pass it to the component:

it('sets focus on the input element', () => {
  const inputRef = React.createRef();
  render(<MyComponent inputRef={inputRef} />);

  act(() => {
    inputRef.current.focus();
  });

  expect(document.activeElement).toBe(inputRef.current);
});

Testing Event Handlers

Use fireEvent to simulate events, ensuring they are wrapped in act:

it('calls onChange handler', () => {
  const onChangeHandler = jest.fn();
  render(<MyComponent onChange={onChangeHandler} />);

  const input = screen.getByRole('textbox');
  act(() => {
    fireEvent.change(input, { target: { value: 'new value' } });
  });

  expect(onChangeHandler).toHaveBeenCalledWith('new value');
});

Performance Optimization

Fast Testing

Reduce rendering depth: Only render the necessary component hierarchy, avoiding rendering the entire component tree.
Use jest.spyOn instead of jest.fn: For performance-sensitive functions, use jest.spyOn instead of jest.fn as it is faster.

Selective Test Execution

Use the --findRelatedTests option to run only tests related to changes, speeding up testing:

npx jest --findRelatedTests

Using Snapshot Testing

For components that rarely change, use snapshot testing to save time:

it('renders snapshot correctly', () => {
  const { container } = render(<MyComponent />);
  expect(container.firstChild).toMatchSnapshot();
});

Code Coverage Threshold

Set code coverage thresholds to ensure sufficient code is tested:

// jest.config.js
module.exports = {
  coverageThreshold: {
    global: {
      statements: 80,
      branches: 80,
      functions: 80,
      lines: 80,
    },
  },
};
0
Subscribe to my newsletter

Read articles from Tianya School directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Tianya School
Tianya School

ā¤ļø • Full Stack Developer šŸš€ • Building Web Apps šŸ‘Øā€šŸ’» • Learning in Public šŸ¤— • Software Developer ⚔ • Freelance Dev šŸ’¼ • DM for Work / Collabs šŸ’¬