Custom Queries for React Testing Library

Matti Bar-Zeev - Feb 10 '23 - - Dev Community

In this post I’m going to share with you a neat testing trick I’ve recently learned about, which can make your React testing a bit easier and in some cases offer a smoother migration to React-Testing-Lib for your project.
Let's jump right to it -


Hey! for more content like the one you're about to read check out @mattibarzeev on Twitter 🍻


Say I have a simple component which displays a list of games. This component knows to display a “loading…” message while loading the games from the mock endpoint.
Here is the code for this component:

import React from 'react';
import {useGames} from '../hooks/UseGames';


export default function Home() {
   const {games, gamesError} = useGames();


   if (gamesError) {
       return <div>Error loading Games</div>;
   }


   if (!games) {
       return <div data-automation="loading-msg">loading...</div>;
   }


   return (
       <>
           {games.map((game) => {
               return (
                   <div key={game?.id}>
                       <h1>{game?.name}</h1>
                       <h3>{game?.genre}</h3>
                   </div>
               );
           })}
       </>
   );
}
Enter fullscreen mode Exit fullscreen mode

I would like to test this component - one of the tests is checking that while there are no games to display the component should display a loading message.
The simple (and probably the correct) way to do that is:

import {render, screen} from '@testing-library/react';
import Home from './Home';


const mock = {games: null, gamesError: null};
jest.mock('../hooks/UseGames', () => ({
   useGames: () => {
       return mock;
   },
}));


describe('Home page', () => {
   it('should render in a loading state', () => {
       render(<Home />);
       const loadingElement = screen.queryByText('loading...');
       expect(loadingElement).toBeInTheDocument();
   });
});
Enter fullscreen mode Exit fullscreen mode

We’re querying the element by its text and then asserting its presence in the document.

But I would like to take this simple example and use it to demonstrate a more complex use-case you might have encountered:
Say that you could not rely on the “loading…” text in order to query the element you wanna test. For the sake of the argument, let's imagine that the only way to get this element is by using some data attribute the element has.

One option we have is using React-Testing-lib’s “getByTestId” method, where it will query the element by the “data-testid” attribute.
But here we don’t have that specific attribute, but we have the “data-automation” attribute. So how do we go about it? Should we add another attribute?
No. There are better ways -

The Configuration approach

Let’s change our test to query by test-id:

describe('Home page', () => {
   it('should render in a loading state', () => {
       render(<Home />);
       const loadingElement = screen.getByTestId('loading-msg');
       expect(loadingElement).toBeInTheDocument();
   });
});
Enter fullscreen mode Exit fullscreen mode

But when we run this test we get an error:

TestingLibraryElementError: Unable to find an element by: [data-testid="loading-msg"]
Enter fullscreen mode Exit fullscreen mode

Which is true, there is no such data attribute.

The configuration approach says that you can change the default data attribute the testing-lib uses in order to query elements by the “getByTestId” method. Here’s how we do that - in the sertupTest.js file we add this content:

import {configure} from '@testing-library/dom';


configure({
   testIdAttribute: 'data-automation',
});
Enter fullscreen mode Exit fullscreen mode

Now when running the test, React-Testing-Lib knows to look for “data-automation” data attribute when getByTestId is used.
Cool.

But what happens when you already have a few tests which query elements with “data-testid” data attribute? Obviously you would not want to damage those by changing the default data attribute.

For this I’d like to share another approach which might come handy in more complex use-cases…

The Custom queries approach

In this approach we’re going to create a custom renderer for React-Testing-lib which will contain our own custom query methods 🙂

(You can read some more details on this approach in the React-Testing-Lib docs)

First we write our custom query. The Query builder does a nice thing - it knows to generate 2 query methods from a single query definition, one for querying a single element and the other for multiple. Here is the code in our custom-queries.js file:

import {queryHelpers, buildQueries} from '@testing-library/react';


const queryAllByDataAutomation = (...args) => queryHelpers.queryAllByAttribute('data-automation', ...args);


const [getByDataAutomation, getAllByDataAutomation] = buildQueries(queryAllByDataAutomation);


export {getByDataAutomation, getAllByDataAutomation};
Enter fullscreen mode Exit fullscreen mode

Now we would like to incorporate this custom query in our testing lib, for that we create a custom-testing-lib.js which will define the custom render, screen and within.
The thing to notice is that we merge the out-of-the-box queries with our custom ones, thus extending the capabilities of our testing lib.

Here how it looks like:

import {render, queries, within} from '@testing-library/react';
import * as customQueries from './custom-queries';


const allQueries = {
   ...queries,
   ...customQueries,
};


const customScreen = within(document.body, allQueries);
const customWithin = (element) => within(element, allQueries);
const customRender = (ui, options) => render(ui, {queries: allQueries, ...options});


// re-export everything
export * from '@testing-library/react';


// override render method
export {customScreen as screen, customWithin as within, customRender as render};
Enter fullscreen mode Exit fullscreen mode

And now I can change my test to use this custom testing-lib like so:

import {render, screen} from '../custom-testing-lib';
import Home from './Home';


const mock = {games: null, gamesError: null};
jest.mock('../hooks/UseGames', () => ({
   useGames: () => {
       return mock;
   },
}));


describe('Home page', () => {
   it('should render in a loading state', () => {
       render(<Home />);
       const loadingElement = screen.getByDataAutomation('loading-msg');
       expect(loadingElement).toBeInTheDocument();
   });
});
Enter fullscreen mode Exit fullscreen mode

Notice I’m importing “render” and “screen” from the custom-testing-lib and using the new query in the test itself.

Running the test, and it passes successfully and… that’s it :)


Hey! for more content like the one you've just read check out @mattibarzeev on Twitter 🍻

Photo by Lucas Kapla on Unsplash

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .