React Testing Library is a test library that helps us write integration and unit tests for our UI applications by allowing us to:
It's built on top of testing-library, which has various integrations besides React, namely Angular, Vue, puppeteer, webDriverIO, Cypress, etc.
The React Testing Library philosophy can be sum up in the following statements:
As general advice, the best mindset to adopt when writing tests with this library is to act as your application user.
For reference, our application can have the following levels of tests:
Query | Type | Returns | When |
---|---|---|---|
queryBy* |
Synchronous | First matching node or null if there is no match. | Good for asserting if an element is not present in the UI |
getBy* |
Synchronous | First matching node or throws an error if there is no match. | Good for assertions of elements we know are already in the UI |
findBy* |
Asynchronous | A promise that resolves when a matching node is found or rejects if there is no match. | Good to wait for elements to be rendered in the UI |
Type | Examples | Characteristics |
---|---|---|
Accessible | getByRole , getByLabelText , getByPlaceholderText , getByText , getByDisplayValue |
Reflect the experience of visual/mouse users as well as those that use assistive technology |
Semantic | getByAltText , getByTitle |
HTML5 and ARIA compliant selectors |
Test IDs | getByTestId |
Reflects implementation details and the user can’t interact with them |
Manual | querySelector |
Same as above and more prone to changes |
All these queries support an `AllBy` variant for getting an array of all matching nodes.
Do I need an expectation after a find? No, but:
UserEvent is a companion library for Testing Library that provides more advanced simulation of browser interactions than the built-in fireEvent method.
For instance, when typing it will first click on the input and then type the text, firing multiple events, closer to what the user will do.
Don’t just wrap the test in act(() => {})
act
by default, ensuring that if you test the component change the warning will disappearSometimes it might be useful to see what elements are present in the DOM. To do so we can use the screen.debug()
instruction inside our test. If the elements printed are too many and get cut you can set the env var DEBUG_PRINT_LIMIT
with a large print limit, e.g. DEBUG_PRINT_LIMIT=30000 yarn test
.
screen.debug()
screen.debug(screen.getByText('test'))
screen.debug(screen.getAllByText('test'))
screen.logTestingPlayground()
screen.logTestingPlayground(screen.getByText('test'))
waitForElementToBeRemove
waits for an element present in the page to be removed;within(screen.getByRole('form')).getByText('test')
searches for an element inside another;There are two plugins that can help a lot in avoiding most of the mistakes I'm about to mention.
The act warning usually means that something in your test is happening which is not tested. Most utilities from React Testing Library are already wrapped in act to mark the behaviors you assert as tested, so there is usually no need to wrap your tests in act.
Asynchronous queries (findBy*
, waitFor
, waitForElementToBeRemoved
, etc.) returns promises and need to have await in front of their invocation, otherwise, the next instruction will be executed before the queries return.
With React Testing Library influencing us towards improving our components accessibility it might be tempting to throw attributes to just make the test work.
In the example below, for instance, getting a form <form></form>
with getByRole("form")
would not work until we add an aria-label. One could force the role manually <form role="role"></form>
and although it would work (if you don't have any a11y eslint plugin in place) the correct form would be <form aria-label="Sign In"></form>
.
My recommendation is to always read the MDN document regarding Roles to better understand what best suits your use cases. There is also a very good website (a11ymatters) which contains the most common UI patterns and the accessibility attributes to use.
The issue with using getBy with expect().not.toBeInTheDocument()
is that if the element does not exist the query will throw an error before it even gets to evaluate if the element is not on the screen. The query queryBy
was created specifically for these use cases, to assert if an element is not in the document without it throwing an error.
The query*
variants should only be used to verify the non-existence of elements, which was the reason they were initially created. For checking the existence of elements, getBy
should be used instead.
fireEvent
and userEvent
are synchronous functions which means we don't need to mark them with await.
find
queries already return a promise, so it's straightforward to await them to resolve in our tests.
If we end up repeating ourselves with these elements we need to wait for we can even extract them to some nice one-line helpers.
waitFor
should be used exclusively to assert if our expectations have been completed, since the waitFor retries multiple times until it passes (or fails after a certain time threshold).