Best Practices
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 testing codebase 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 Solution
The react-testing-library is a very lightweight solution for testing React components. It provides light utility functions on top of react-dom and react-dom/test-utils, in a way that encourages better testing practices. Its primary guiding principle is: The more your tests resemble the way your software is used, the more confidence they can give you.
TLDR: Only test what a user would do, do not test implementation details like state change.
The Strategy
- Test that your props, state, and data are all getting rendered correctly on the DOM using
.toBeInTheDocument()or.toExist() - Test that any user interaction with your component renders the correct data change on the DOM using
.toBe(),.toBeTruthy/Falsy(), .toHaveLength(), or.toEqual() - Test any side effects, such as making sure the correct function was called with the correct arguments with
.toBeCalledWith(myArguments) - Avoid hardcoding any data and use libraries such as faker
- Mock out any trivial component that you need to import but don’t need to test to speed up tests.
Always ask yourself if what you’re testing is an implementation detail.
Read this article on common mistakes with RTL, help us write good tests!
RTL Queries precedence
byRole vs byText
The reason why byRole is a better option to query what the user sees, is because querying by accessible name ignores dom structure. This means less time refactoring our test for an implementation change.
Example,
<button>
<span>Hello </span>
<span>World</span>
</button>
Doing screen.getByText("Hello World") wouldn't return the button since it has two other children. but screen.getByRole("button", {name: "Hello World"}) would, since the dom understands that the accessible name of the button is "Hello world" and is what we are querying for.