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
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,
},
},
};
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 š¬