DEV Community

Cover image for React Testing Library
Paul
Paul

Posted on • Edited on

React Testing Library

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 and ByDisplayValue, as they reflect the user experience the most.
  • If needed use semantic queries like ByAltText or ByTitle, as the user experience can vary for these attributes across various browsers.
  • Avoid using data-testid with ByTestId 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.

Alt Text

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,
      },
    }
  );
Enter fullscreen mode Exit fullscreen mode

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,
  };
};

Enter fullscreen mode Exit fullscreen mode

Top comments (0)