EXPEDIA GROUP TECHNOLOGY — SOFTWARE
UI Testing with React Testing Library and Jest
A lightweight approach to testing React applications
Introduction
In my team, we used to write a lot of UI unit tests in our Backbone application. Unfortunately, they were not very resilient to code changes or new use cases.
Recently, we have been creating React components. So we had an opportunity to re-think unit testing. Soon we realized there are two kinds of unit testing we can do:
- Test the internal working of React components with Jest & Enzyme
- Test the component from an end-user perspective with Jest & React Testing Library
We decided to go with the second type.
These are a few UI unit testing guidelines that are inspired by the React Testing Library philosophy:
https://testing-library.com/docs/guiding-principles
Avoid testing implementation details
- Tests shouldn’t break for every small code change (refactoring) that doesn’t change the UI
- When tests focus on implementation details, they are hard to maintain
- Tests are an alternate way of documenting how the code renders for the user
Testing as an end user and as a developer
- End user interacts with the component
- Developer user renders the component
- Avoid a test user in unit tests who tests any implementation details
- Tests should reflect how an end-user might see and interact with the component as closely as possible
There is no need to have 100% code coverage
- We should have good coverage numbers but it’s almost useless and will lead to brittle/slow testing if we go for 100% code coverage
Quick recap of Jest
Jest is a testing framework for testing JavaScript. It’s very well documented and requires very little configuration to get started.
We will use Jest as the test runner so let’s go over a few basic Jest concepts very briefly. This is an example of a test:
In the above test:
describe
function is a test suitetest
function is a test caseexpect
function is an assertion
We are testing a function called hello()
, which is in another file, so we import it into hello.spec.js
and call it.
React Testing Library
This is a really nice lightweight testing library for testing React components by querying and interacting with DOM nodes. It has very easy utility methods to simulate an end-user. It also works very well together with Jest.
Let’s say we have a SearchForm
React component which may consist of input fields and a search button. To test it, we could write:
Here in the above test, we have two methods from @testing-library/react
:
render
is a utility method provided by React Testing Library. It renders a react component into a container and appends todocument.body
.screen
is an object which is exported by RTL (React Testing Library) and has access to all the query methods that check HTML elements visible indocument.body
.
RTL: Query methods
byRole
This will query elements with a certain role that they have been assigned. Here is a good page for reference to HTML elements and their roles.
For example <a href>
will always have a “link” role, while <button>
will have a “button” role.
byText
This will look for elements having a particular text content.
byLabelText
This will try to match a label present in the HTML and return an element that is associated with that label.
byPlaceholderText
This will look for elements with a placeholder attribute and match the text.
There are a few more query methods documented here.
RTL: Search variants
getBy
This method returns either an element or an error if an element cannot be found. This way, the test can fail fast and early, but it is difficult to test for elements that are not supposed to be there.
E.g.: getByRole
queryBy
This is the same as getBy but we can also assert for elements that are not present without getting an error.
E.g.: queryByRole
, queryByLabelText
findBy
This is to assert for any elements that aren’t there yet but will be eventually. This returns a promise, which gets resolved when an element is found.
E.g.: findByText
Multiple elements
All the below variants return an array of elements.
getAllBy
queryAllBy
findAllBy
RTL: Assertions
Usually, assertions come from Jest. React Testing Library extends the Jest assertions to include more focused DOM assertions.
These are some of the assertions that we have used so far:
toBeVisible()
toBeEnabled()
toBeDisabled()
toHaveTextContent()
Check out the rest of the assertions in thejest-dom
package.
RTL: User interactions
React Testing Library also provides a user event library: @testing-library/user-event
.
The userEvent
API fires events in a way that mimics the actual browser behavior. If we were testing the click of a search button in our search form example above, it would look like this:
Testing asynchronous behavior with React Testing Library
React testing Library provides an async utility method called waitFor
, which we are in this test. This is particularly useful for a unit test that mocks the API and waits for any mock promises to resolve.
//Wait till the mock API gives back a successful result
await waitFor(() => screen.getByText("Search Results"),
{ timeout: 3000 });
Mocking
How to mock modules/components
With Jest, we can create a mock for the entire module/service/component by creating a mock file with the same name and placing it inside __mocks__
folder.
jest.mock()
takes the path to the module that we are mocking and when it encounters the module in the test, it swaps out the real module with the mocked module.
How to mock functions
Jest also allows for us to mock functions using jest.fn()
.
Here is an example of a test with mocks and async behavior testing:
Wrapping up
I really liked React Testing Library because it forces the developer to ignore the implementation details and really test user behavior. Having written our unit tests in React Testing Library, we are confident that if any of our tests were to fail after a code change, it would mean that an existing end-user experience has changed. This will make us more conscious of the changes we make in either the UI code or in the test to support the new changes because now our tests closely resemble real end user scenarios.