DEV Community

Tianya School
Tianya School

Posted on

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

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
Enter fullscreen mode Exit fullscreen mode

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

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

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();
  });
});
Enter fullscreen mode Exit fullscreen mode

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));
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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());
});
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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());
});
Enter fullscreen mode Exit fullscreen mode

Clear Test Descriptions

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

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

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();
});
Enter fullscreen mode Exit fullscreen mode

Code Coverage Report

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

npx jest --coverage
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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,
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
om_shree_0709 profile image
Om Shree

Nice