Why switch from enzyme to react-testing-library?
While enzyme is intended for unit/integration tests, react-testing-library is for blackbox integration tests. Enzyme gives you access to the internal workings of your components and seduces you to test implementation details. Through the blackbox approach of react-testing-library you are forced to write more user-centered tests and interact with the components from the outside instead of accessing implementation details.
Guiding principles
- If it relates to rendering components, then it should deal with DOM nodes rather than component instances, and it should not encourage dealing with component instances.
- It should be generally useful for testing the application components in the way the user would use it. We are making some trade-offs here because we're using a computer and often a simulated browser environment, but in general, utilities should encourage tests that use the components the way they're intended to be used.
- Utility implementations and APIs should be simple and flexible.
Guidelines
- Use expect explicitly with
toBeInTheDocument()
when testing if element exists with getBy or one of the other queries. - Only import the
testUtils.js
never import@testing-library/react
- Prefer the queries
ByLabelText
,ByPlaceholderText
,ByRole
,ByText
andByDisplayValue
, as they reflect the user experience the most. - If needed use semantic queries like
ByAltText
orByTitle
, as the user experience can vary for these attributes across various browsers. - Avoid using
data-testid
withByTestId
as much as possible, because it requires to change the component just for the test. - Focus on writing user-centered tests and avoid testing implementation details. Expect what the user will see.
Apollo, Intl, Redux
To efficiently wrap our test cases in the needed Providers, a custom render function was implemented. This function wraps our test component in Redux, Intl and Apollo. It takes the apolloMocks and the redux initialStore as arguments, to set them as needed in our test case.
The apolloMocks data has to match the query datatype completely otherwise they won't resolve in the test.
Api
A very good resource to get a fast overview is the testing-library cheatsheet.
Queries
The following queries are provided to select elements in the DOM:
- ByLabelText
- ByPlaceholderText
- ByText
- ByDisplayValue
- ByAltText
- ByTitle
- ByRole
- ByTestId
These need to be combined with the table of selectors below.
E.g. getByLabelText
.
For ByText you can use regex to find all elements that contain a fitting text.
Difference get, query, find
No Match | 1 Match | 1+ Match | Await? | |
---|---|---|---|---|
getBy | throw | return | throw | No |
findBy | throw | return | throw | Yes |
queryBy | null | return | throw | No |
getAllBy | throw | array | array | No |
findAllBy | throw | array | array | Yes |
queryAllBy | [] | array | array | No |
Async
The library provides multiple functions to deal with asynchronous code, like wait
and waitForElementToBeRemoved
. These functions take a callback and an options object as parameters. In the options, you can set a timeout (default: 1000ms) and an interval (default: 50ms) to check the callback function.
Events
Testing events is done using the fireEvent(node, event)
function.
It has multiple helpers for default events that can be used like: fireEvent.click(node)
.
Jest-Dom
Jest-Dom provides us with additional jest matchers for testing.
Some useful examples are:
- toBeInTheDocument
- toHaveTextContent
- toHaveValue
- toBeVisible
- toContainElement
Our additions
To select text elements that are filled with intl ids a custom selector was implemented, that can be used with all selector types in the table.
E.g. getByIntlId
.
Code to copy
Some of the custom code to adapt the framework to our needs.
Custom renderer
const customRender = (
ui: React$Element<*>,
{ apolloMocks = [], initialStore = {} }: WrapperParameterType = {}
) =>
render(
<Provider store={configureStore(initialStore)}>
<RawIntlProvider value={intl}>
<MockedProvider mocks={apolloMocks} addTypename={false}>
{ui}
</MockedProvider>
</RawIntlProvider>
</Provider>,
{
queries: {
...queries,
...intlQueries,
},
}
);
Custom query
export const buildIntlQueries = (intl: string) => {
const queryAllByIntlId = (container, intlId, options, ...rest) => {
const text = intl.formatMessage({ id: intlId, ...options });
return queryAllByText(container, text, ...rest);
};
const getMultipleError = (_, intlId) =>
`Found multiple elements with the intl id: ${intlId}`;
const getMissingError = (_, intlId) =>
`Unable to find an element with the intl id: ${intlId}`;
const [
queryByIntlId,
getAllByIntlId,
getByIntlId,
findAllByIntlId,
findByIntlId,
] = buildQueries(queryAllByIntlId, getMultipleError, getMissingError);
return {
queryByIntlId,
getAllByIntlId,
getByIntlId,
findAllByIntlId,
findByIntlId,
};
};
Top comments (0)