React Testing Library
Enzyme (before React-testing-library)
- Shallow vs Deep rendered components
- Direct testing of prop values on components
- True isolated "unit" testing
"The Problem: You want to write maintainable tests for your React components. As a part of this goal, you want your tests to avoid including implementation details of your components and rather focus on making your tests give you the confidence for which they are intended. As part of this, you want your testbase to be maintainable in the long run so refactors of your components (changes to implementation but not functionality) don't break your tests and slow you and your team down."
The more your tests resemble the way your software is used, the more confidence they can give you.
Forces you to test the DOM in a way the user would interact with your code
- Labels and text
- Roles
- Find elements by a data-testid as an "escape hatch" for elements where the text content and label do not make sense or is not practical
Pros and Cons
React Testing Library
Enzyme
Pros
Cons
Testing the way users interact with the site
Harder to test specific use cases
easy to quickly test use cases
Duplicated coverage
(Tons of mocks)
Integration-focused/focus on real scenarios
testing implementation details
constantly having to "fix" tests
tons of utilities / very powerful
What to test?
Think in User Flows and Boundaries
Write fewer, longer, more comprehensive tests
Test boundary - The API loaded, user makes changes, clicks submit, and then saw a success message
[GET API] -> [UI screen] -> [Updates] -> [Button Click] -> [Success Message] -> [POST API]
"Business Logic"
Heavy business logic should be tested at the unit level
Business Logic -> If / elses
Mapping or other complicated logic (if elses) should have a unit test (functions that aren't react components)
Put complicated logic in hooks - and test that (it's much easier)
100% coverage?
Best Practices
Selectors (priority order)
-
Queries Accessible to Everyone
- getByRole
- getByLabelText
- getByPlaceholderText
- getByText
- getByDisplayValue
-
Semantic Queries
- getByAltText
- getByTitle
-
Test IDs
- getByTestId
GetBy vs QueryBy vs FindBy
explicit assertion
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
render(<App />);
// implicit assertion
// because getByText would throw error
// if element wouldn't be there
screen.getByText('Search:');
// explicit assertion
// recommended
expect(screen.getByText('Search:')).toBeInTheDocument();
});
});
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
const { container } = render(<App />);
/** don't do this **/
const myElement = container.querySelectorAll('.some-class');
});
});
🚨 If you find yourself using container to query for rendered elements then you should reconsider! The other queries are designed to be more resilient to changes that will be made to the component you're testing. Avoid using container to query for elements!
events
Most projects have a few use cases for fireEvent, but the majority of the time you should probably use @testing-library/user-event.
callbacks
describe('Search', () => {
test('calls the onChange callback handler', () => {
const onChange = jest.fn();
render(
<Search value="" onChange={onChange}>
Search:
</Search>
);
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'JavaScript' },
});
expect(onChange).toHaveBeenCalledTimes(1);
});
});
How do I fix "an update was not wrapped in act(...)" warnings?
This warning is usually caused by an async operation causing an update after the test has already finished. There are 2 approaches to resolve it:
- Wait for the result of the operation in your test by using one of the async utilities like waitFor or a find* query. For example: const userAddress = await findByLabel(/address/i).
- Mocking out the asynchronous operation so that it doesn't trigger state updates.
Generally speaking, approach 1 is preferred since it better matches the expectations of a user interacting with your app.
Debugging
Debugging
screen.debug()
import { screen } from '@testing-library/dom'
document.body.innerHTML = `
<button>test</button>
<span>multi-test</span>
<div>multi-test</div>
`
// debug document
screen.debug()
// debug single element
screen.debug(screen.getByText('test'))
// debug multiple elements
screen.debug(screen.getAllByText('multi-test'))
screen.logTestingPlaygroundURL()
import { screen } from '@testing-library/dom'
document.body.innerHTML = `
<button>test</button>
<span>multi-test</span>
<div>multi-test</div>
`
// log entire document to testing-playground
screen.logTestingPlaygroundURL()
// log a single element
screen.logTestingPlaygroundURL(screen.getByText('test'))
Resources
React Testing Library
By Alex Voerman
React Testing Library
- 246