Testing React Components
React Testing Library is a popular testing library that allows developers to write tests for React components. It aims to provide a way to test components in a way that more closely resembles how they are used by real users.
Resources
Set Up
First, set up Jest for TypeScript.
-
Add
jest-dom
package for additional test helper functions -
import '@testing-library/jest-dom/extend-expect'
to use utility functions liketoBeInTheDocument()
yarn add -D @testing-library/jest-dom
Then add an import to your jest setup file
// jest.config.js
module.exports = {
clearMocks: true,
collectCoverage: true,
coverageDirectory: 'coverage',
coveragePathIgnorePatterns: ['/node_modules/'],
coverageProvider: 'v8',
preset: 'ts-jest',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testEnvironment: 'jsdom',
transform: {
'^.+\\.(ts|tsx)?$': 'ts-jest',
'^.+\\.(js|jsx)$': 'babel-jest',
},
}
// jest.setup.js
import '@testing-library/jest-dom/extend-expect'
An example test is like this
import * as React from 'react'
import { render, screen } from '@testing-library/react'
import List from '../src/components/List'
import '@testing-library/jest-dom/extend-expect'
describe('List', () => {
const milk = 'Milk'
const eggs = 'Eggs'
const items = [
{ name: milk, done: false },
{ name: eggs, done: true },
]
it('renders a list of items', () => {
render(<List items={items} toggle={jest.fn()} />)
expect(screen.getByText(milk)).toBeTruthy()
expect(screen.getByRole('checkbox', { name: milk })).not.toBeChecked()
expect(screen.getByText(eggs)).toBeTruthy()
})
})
User Interactions
user-event
is a companion library for Testing Library that simulates user interactions by dispatching the events that would happen if the interaction took place in a browser. Take note that all the functions return promise
so use it with await
yarn add -D @testing-library/user-event
-
type
-
click
import * as React from 'react'
import { render, screen } from "@testing-library/react"
import Add from '../src/components/Add'
import userEvent from '@testing-library/user-event'
describe('Add', () => {
const user = userEvent.setup()
const addToItems = jest.fn()
const item = 'Milk'
it('calls addToItems when the button is clicked', async () => {
render(<Add addToItems={addToItems} />)
await user.type(screen.getByRole('textbox'), item)
await user.click(screen.getByRole('button'))
expect(addToItems).toHaveBeenCalledWith({ name: item, done: false })
})
})
await userEvent.click(screen.getByRole('radio', { name: 'Yes' }));
await userEvent.selectOptions(
screen.getByRole('combobox'),
'Indication or potential indication of significant harm'
);
await userEvent.type(
screen.getByRole('textbox', { name: 'Agreed action' }),
agreedActionDescription
);
await userEvent.click(screen.getByRole('checkbox'));
await userEvent.click(screen.getByRole('button', { name: /next/i }));
Selectors
getByRole
-
textbox
: input, textarea -
checkbox
: checkbox -
radio
: radio button -
button
: button -
combobox
:<select />
One thing to note is name
doesn’t mean the name attribute in the element. It’s the text in the associated label.
// select a button with the label
const submitButton = screen.getByRole('button', { name: /ACTION/i })
it('save button should be enabled when all required inputs are done', async () => {
await act(async () => {
renderWithKompass(<AdminNoteForm formMode={FORM.CREATE} onSubmit={jest.fn()} />);
await userEvent.type(
screen.getByRole('textbox', { name: 'Note description' }),
'Note description'
);
expect(screen.getByRole('button', { name: 'SAVE' })).toHaveProperty('disabled', false);
});
});
expect(screen.getByRole('textbox', { name: 'Concern' })).toHaveValue('Concern description')
expect(screen.getByRole('textbox', { name: 'Agreed action' })).toHaveValue(
'Agreed action description'
);
expect(screen.getByRole('radio', { name: 'Yes' })).toBeChecked();
expect(screen.getByRole('combobox', { name: 'Primary reason for review' })).toHaveValue(
'Indication or potential indication of significant harm'
);
Assertions
toBeDefined()
Ensure that a variable is not undefined
it('should render the label text', () => {
renderWithTheme(<ConfirmationBox {...props} />);
expect(screen.getByText('I confirm that I have followed the')).toBeDefined();
});
toBeInTheDocument()
Check if an element is in the document
import * as React from 'react'
import { render, screen } from "@testing-library/react"
import Add from '../src/components/Add'
import userEvent from '@testing-library/user-event'
describe('Add', () => {
const user = userEvent.setup()
const addToItems = jest.fn()
const item = 'Milk'
it('renders an input and a button', () => {
render(<Add addToItems={addToItems} />)
expect(screen.getByRole('textbox')).toBeInTheDocument()
expect(screen.getByRole('button')).toBeInTheDocument()
})
it('displays an error message when the item is empty', async () => {
render(<Add addToItems={addToItems} />)
await user.click(screen.getByRole('button'))
expect(screen.getByText('The item cannot be empty.')).toBeInTheDocument()
})
})
toHaveBeenCalledWith()
import * as React from 'react'
import { render, screen } from "@testing-library/react"
import Add from '../src/components/Add'
import userEvent from '@testing-library/user-event'
describe('Add', () => {
const user = userEvent.setup()
const addToItems = jest.fn()
const item = 'Milk'
it('calls addToItems when the button is clicked', async () => {
render(<Add addToItems={addToItems} />)
await user.type(screen.getByRole('textbox'), item)
await user.click(screen.getByRole('button'))
expect(addToItems).toHaveBeenCalledWith({ name: item, done: false })
})
it('does not call addToItems when the button is clicked but the item is empty', async () => {
render(<Add addToItems={addToItems} />)
await user.click(screen.getByRole('button'))
expect(addToItems).not.toHaveBeenCalled()
})
})
toBeChecked()
When a radio button is selected
<input
type="radio"
name="requireReview"
value="true"
>
<label>Yes</label>
</input>
expect(screen.getByRole('radio', { name: 'Yes' })).toBeChecked();
waitFor()
Wait for a while to evaluate the expression
await waitFor(() => expect(screen.getByRole('textbox', { name: 'Concern' }))
.toHaveValue('Concern description')
);
Gotchas
react-script test is not finding any test
The react-scripts test
command is used to run tests in a React project created with create-react-app
. If you are running this command and it is not finding any tests, there could be a few possible reasons:
-
Test files are not named correctly: By default,
create-react-app
looks for files with the.test.(j|t)s
or.spec.(j|t)s
file extension to run tests. Make sure your test files are named appropriately. -
Test files are not in the correct directory: By default,
create-react-app
looks for test files in thesrc
directory. Make sure your test files are located in thesrc
directory or a subdirectory ofsrc
.
The input element doesn’t have the new value
Make sure you find the element again to check the new value. Don’t use a variable that holds the element as the value of the input is not updated.
it('clears the input when the button is clicked', async () => {
render(
<MockedProvider mocks={mocks} addTypename={false}>
<Add />
</MockedProvider>
)
await user.type(screen.getByRole('textbox'), item)
await user.click(screen.getByRole('button'))
await waitFor(() => expect(screen.getByRole('textbox')).toHaveValue(''))
// do screen.getByRole('textbox') again.
})
Comments