-
Notifications
You must be signed in to change notification settings - Fork 355
React Testing Library Basics, Best Practices, and Guidelines
Read the RTL (React Testing Library) docs intro page for the quick “why” behind RTL. “The RTL way” of testing is primarily behavior-driven, with a focus on not testing implementation details of a component.
RTL’s render
method is used to render components for testing purposes. Much like Enzymes 'mount` method it renders not only the direct component under test, but all children of that component as well.
RTL does not have any shallow rendering functionality, because of its focus on testing as the user would use an application. If you need to effectively shallow render something you will have to mock the child components, see the section on mocking below for more information on this.
Rather than assigning the value of render
to a variable like we would with shallow
or mount
from Enzyme; we can instead call queries (and some other functions) on RTL’s screen
object.
Using screen
simplifies test writing because it allows you to use render
in one way rather than having to destructure it differently depending on what you want to test.
The exception to using screen
is that snapshot testing does require destructuring render
s return value to access its asFragment
property.
In RTL documentation you will see mentions of both userEvent
and fireEvent
,userEvent
is the simulation library recommended by the RTL team as its simulations are designed to be more advanced/realistic than the simulations provided by fireEvent
. See the userEvent API docs for more information.
Calling act()
is typically not needed in RTL, because RTL uses it behind the scenes by default, including when using the userEvent
API.
You may occasionally get a warning in your terminal stating "an update was not wrapped in act(...)" when you run your tests, the most likely cause is an async operation updating after completion of the test. Some recommended things you can try to solve this error (in order of preference) include:
- Use a
find
query rather than aget
query. - If a find query can’t be used the
waitFor
method is the next item that should be attempted. - If neither of the above resolves the warning, the operation causing the issue should be mocked.
- If the operation causing the issue can’t be mocked out you can wrap the action in
act()
.
Note: The use of Jest's fake timer functionality is an exception to the above guideline, if time passing causes state updates you should wrap your advanceTimersByTime
calls in act()
.
-
getBy
/getAllBy
should be the queries you reach for by default. -
queryBy
/queryByAll
should only be used to assert that something is not present, as in that situationgetBy
will throw an error. -
findBy
/findAllBy
should be used when the item you’re querying for may not be immediately available. Note that these will return a promise rather than the element/elements queried.
RTL maintains an explicit query priority list for what queries you should attempt to use over others
getByRole
should be the default query that you use in most situations. Its name option allows you to somewhat combine getByRole
with getByText
, enabling you to select the vast majority of things you may need. The name option follows the format getByRole(‘heading’, { name: ‘Text visible in the component’ })
The custom matchers provided by jest-dom (which come from the @testing-library/jest-dom package) should generally be used for DOM-based assertions rather than defaulting to the more simplistic matchers provided by default in Jest.
Appropriate/more specific matchers should be leveraged as much as possible, for example it’s encouraged to use expect(button).toBeDisabled()
vs expect(button.disabled).toBe(true)
.
Because RTL doesn’t support shallow rendering some components may need to be mocked for a pure unit testing approach. RTL itself doesn’t add mocking capabilities, but the mocking basics are being covered here because we may need to do so to replace some tests that previously used shallow
.
For the component:
const MyButton = ({ onClick }) => <button onClick={onClick}>Text</button>;
Adequate testing for this component’s functionality would ensure that the provided onClick
callback prop is called when expected, and also that it’s not called when not expected. It may look like:
describe('functionality', () => {
it('calls its onClick callback when clicked', () => {
// define the mock function
const onClickMock = jest.fn();
// render the component with our mock
render(<MyButton onClick={onClickMock}>Text</MyButton>);
// find the button and click it using userEvent
userEvent.click(screen.getByRole('button', { name: 'Text' }));
// assert that it was called as many times as we expect
expect(onClickMock).toHaveBeenCalledTimes(1);
});
it('does not call its onClick callback when not clicked', () => {
const onClickMock = jest.fn();
render(<MyButton onClick={onClickMock}>Text</MyButton>);
// assert that it wasn't called because in this test we didn't click the button
// this prevents false-positive calls in addition to the false negatives prevented by the first test
expect(onClickMock).not.toHaveBeenCalled();
});
});
For the component:
import { RandomHeader } from './RandomHeader';
export const MyPage = () => (
<div>
<RandomHeader />
Body text
</div>
);
Where <RandomHeader />
is a child that returns a header with random text, some testing approaches (such as snapshots) will be impossible without mocking. In order to test this component we can do:
jest.mock('../RandomHeader', () => () => <h1>Header text</h1>);
describe('rendering', () => {
it('matches the snapshot', () => {
const { asFragment } render(<MyPage />);
expect(asFragment()).toMatchSnapshot();
});
});
Notice that here jest.mock
takes two arguments, the path to the component you’re mocking from the test file, and a function that returns another function that returns the JSX being used in your mock.
If you need a mocked child component to take children or other props which you can test against you can use the following alternative mock setup to do so:
jest.mock('../Header', () => ({
Header: ({ children, ...props }) => <h1 {...props}>{children}</h1>
});
When the component you’re testing relies on a context, you can either import the actual context that will be used or mock the context similarly to how a child component is mocked.
Whichever way you choose you will need to provide a value
prop to the Context.Provider
with the needed values, with your component under test as a child. This is done in the same way you would provide the values to the context in real-world usage.
Snapshots should not be used to assert that a specific class is applied to a component. Tests should explicitly check for the class names that we expect to exist (for instance by using jest-dom's toHaveClass matcher).
Snapshots should be used in situations where a component's structure is the primary concern of the test, i.e. for testing that elements are in the correct order.
Whether to abstract elements of test setup from each test block or leave all of a tests setup in the associated test block is a highly variable decision with no hard rules.
Our current guidance is to default to not abstracting setup unless that setup "excessively" bloats the test suite, or the abstraction helps clarify the purpose of the test by making it easier to distinguish what elements of the setup are changing for each test.
Current guidance is to default to mocking child components for unit testing of the props a component is passing.
Rather than having a single monolithic test file for each component as they appear in our component list, a separate test file should be used for each individual component that we export.
Test cases do not need to all be wrapped in describe()
blocks which only state the component they're for. describe()
blocks should be used to group test cases together within a test suite when needed, i.e. when using a shared setup.
Use test()
for your test cases rather than it()
when not inside of a describe()
block.
- Read this page about migrating from Enzyme to RTL for a longer (but very good) rundown by the RTL team themselves.
- This article about some common RTL mistakes by one of the RTL developers is also very good.