React Testing Library Usage
Resources
Test Boilerplate
import {render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import SomeComponent from './SomeComponent';
test('test description goes here', async () => {
// ARRANGE
render(<SomeComponent />);
// ACT
await userEvent.click(screen.getByText('Load Greeting'));
await screen.findByRole('heading');
// ASSERT
expect(screen.getByRole('heading')).toHaveTextContent('hello there');
expect(screen.getByRole('button')).toBeDisabled();
});
Rules of Thumb
Target using roles.
We always want to target elements using their roles if possible, as this seperates the test from the implementation details.
In my experience however this is also the most difficult part of testing with React Testing Library, as it can be difficult to correctly target element using roles.
Smoke Test
Always pop a smoke test in there first
import React from 'react';
import {render} from '@testing-library/react';
import DataTable from './DataTable';
test('DataTable component loads without crashing', () => {
render(<DataTable />);
});
Additionally if you want to have some kind of test on a component, but don't have the time to write a more comprehensive test, you can always just start with a smoke test and then add more tests later. It's a good way to get started.
Difference between act
and waitFor
act
is used to wrap code that causes side effects, such as rendering, fetching data, or updating the DOM. It is used to make sure that the side effects are completed before the test continues.
import {render} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import MyComponent from './MyComponent';
test('button click updates text', () => {
const {getByText} = render(<MyComponent />);
const button = getByText('Click me');
await act(() => {
userEvent.click(button);
});
expect(getByText('Clicked')).toBeInTheDocument();
});
waitFor
Waits for an expectation to be met, typically used for assertions that depend on asynchronous operations. Used when you need to wait for an element to appear, disappear, or change state in the DOM after an asynchronous operation.
import {render, screen, waitFor} from '@testing-library/react';
import MyComponent from './MyComponent';
test('loads and displays greeting', async () => {
render(<MyComponent />);
await waitFor(() => {
expect(screen.getByText('Hello, World!')).toBeInTheDocument();
});
});
Sample Tests
Here is the component we will be testing. It's a Data Table component build with React-Table v8 (typescript version!), which has a search box in one column, a custom multi-select filter in another column, and some pagination features.
Domain | Type | Description |
---|---|---|
.abbott | Brand | Associated with Abbott Laboratories, for their use |
.abogado | gTLD | Spanish for "lawyer", used by legal professionals |
.ac | ccTLD | Academic institutions, primarily used in Ascension Island |
.academy | gTLD | Educational institutions and academies |
.accountant | gTLD | Used by accountants and accounting firms |
.accountants | gTLD | Used by accountants and accounting firms |
.active | gTLD | General use, often associated with active lifestyles |
.actor | gTLD | Actors and acting industry |
.ad | ccTLD | Country code for Andorra, general use in advertising |
.ads | gTLD | Short for advertisements, used in the advertising industry |
domain | type | description |
and here are some tests for It
import {
render,
screen,
waitFor,
within,
fireEvent,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import DomainsTable from '@site/src/components/tables/DomainsTable.tsx';
// smoke test
test('DataTable component loads without crashing', async () => {
// ARRANGE
render(<DomainsTable />);
});
// search renders
test('renders search input', () => {
render(<DomainsTable />);
const searchInput = screen.getByPlaceholderText(/Search\.\.\. \(\d+\)/i);
expect(searchInput).toBeInTheDocument();
});
// column search filter test
test('column search correctly filters rows', async () => {
render(<DomainsTable />);
const searchInput = screen.getByPlaceholderText(/Search\.\.\. \(\d+\)/i);
// search for '.airforce'
userEvent.click(searchInput);
userEvent.type(searchInput, '.airforce');
await waitFor(() => {
// there should be one row in the table body
const table = screen.getByRole('table');
const tbody = within(table).getByTestId('data-table-body');
const rows = within(tbody).getAllByRole('row');
const firstRow = rows[0];
const cells = within(firstRow).getAllByRole('cell');
expect(rows.length).toBe(1);
expect(cells[0]).toHaveTextContent('.airforce');
});
});
// column multiselect filter test
test('column multiselect filter correctly filters rows', async () => {
render(<DomainsTable />);
const multiSelectFilterButton = screen.getByTestId(
'multi-select-filter-button',
);
// Simulate a button click on multi-select filter
userEvent.click(multiSelectFilterButton);
// find multi-select popover
// Wait for the multi-select popover to appear
const listbox = await waitFor(() =>
screen.getByTestId('multi-select-popover'),
);
const infrastructureOption = within(listbox).getByRole('option', {
name: /infrastructure/i,
});
// click the infrastructure filter option
userEvent.click(infrastructureOption);
await waitFor(() => {
const table = screen.getByRole('table');
const tbody = within(table).getByTestId('data-table-body');
const rows = within(tbody).getAllByRole('row');
const firstRow = rows[0];
const cells = within(firstRow).getAllByRole('cell');
expect(rows.length).toBe(1);
expect(cells[1]).toHaveTextContent('infrastructure');
});
});
// pagination test
test('pagination elements have all rendered and perform expected actions', async () => {
const user = userEvent.setup();
render(<DomainsTable />);
const pagination = screen.getByRole('pagination');
const paginationButtons = within(pagination).getAllByRole('button');
const paginationInfo = screen.getByRole('pagination-info');
const paginationPageSizeSelect = screen.getByRole(
'pagination-page-size-select',
);
expect(pagination).toBeInTheDocument();
expect(paginationButtons).toHaveLength(4);
expect(paginationInfo).toBeInTheDocument();
expect(paginationPageSizeSelect).toBeInTheDocument();
// click the third pagination button (go forward one page)
user.click(paginationButtons[2]);
await waitFor(() => {
// Use a regular expression to match the text "Page 2 of x" where x is any number, ignoring spaces and case
expect(paginationInfo).toHaveTextContent(/page\s*2\s*of\s*\d+/i);
});
user.click(paginationPageSizeSelect);
const pageSizeOptions = within(paginationPageSizeSelect).getAllByRole(
'option',
);
// select show 20 rows
// I could not get this .click method to actually change the value of the select, odd
//user.click(pageSizeOptions[1]);
fireEvent.change(paginationPageSizeSelect, {target: {value: '20'}});
// make sure there are 20 rows in the table
await waitFor(() => {
const table = screen.getByRole('table');
const tbody = within(table).getByTestId('data-table-body');
const rows = within(tbody).getAllByRole('row');
// Log the rows for debugging
//console.log(rows);
// We should have 20 rows
expect(rows.length).toBe(20);
});
});
Comments
Recent Work
Basalt
basalt.softwareFree desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.
BidBear
bidbear.ioBidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.