Custom Queries for React Testing Library
Tags: react, tutorial, testing, webdev
Published: 2023-02-10 09:31:30

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 <a href="https://twitter.com/mattibarzeev?ref_src=twsrc%5Etfw" class="twitter-follow-button" data-show-count="false">@mattibarzeev</a><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> on Twitter* :beers:

***

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:

```javascript
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>
               );
           })}
       </>
   );
}
```

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:

```javascript
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();
   });
});
```

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](https://testing-library.com/docs/queries/bytestid/), 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:

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

But when we run this test we get an error:

```bash
TestingLibraryElementError: Unable to find an element by: [data-testid="loading-msg"]
```

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:

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


configure({
   testIdAttribute: 'data-automation',
});
```

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](https://testing-library.com/docs/react-testing-library/setup#configuring-jest-with-test-utils))

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:

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


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


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


export {getByDataAutomation, getAllByDataAutomation};
```

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:

```javascript
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};
```

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

```javascript
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();
   });
});
```

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 <a href="https://twitter.com/mattibarzeev?ref_src=twsrc%5Etfw" class="twitter-follow-button" data-show-count="false">@mattibarzeev</a><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> on Twitter* :beers:

<small><small><small>Photo by <a href="https://unsplash.com/@aznbokchoy?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Lucas Kapla</a> on <a href="https://unsplash.com/photos/wQLAGv4_OYs?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></small></small></small>