"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.
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
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]
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)
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!
Most projects have a few use cases for fireEvent, but the majority of the time you should probably use @testing-library/user-event.
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);
});
});
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:
Generally speaking, approach 1 is preferred since it better matches the expectations of a user interacting with your app.
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'))
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'))