Tips for Using React Testing Library to Write Unit Tests

Writing unit tests and testing in general is an integral part of any software development project. It lets you know your code is working as expected and serves as a warning sign before you push buggy code. At Atomic Object, we like to practice something called TDD (test-driven development). It is a great way to write performant and well-functioning code. The concept is straightforward, write tests based on what the code should do and how it should do that. Then write the implementation code so that it passes those tests.

It’s great and you can be sure that, while you are developing, you are moving in the right direction. This assumes that you wrote your tests properly with the right assertions and setup. But we are not here to talk about how to properly write unit tests overall. We are here to talk about what I think are some really helpful tips for using the React Testing Library package to write good tests that provide accurate feedback and don’t bog down your testing suite.

Pick the correct query.

When using React testing library, you need to query rendered elements for assertions. You can choose from three types of queries: getBy, findBy, and queryBy. Each of these will, essentially, look at the DOM from your render call in your test to see if an element exists and return that element and its properties. That is not an exaggeration. In essence, they all do the same thing. So, how do we know which query to use and when? The simple and straightforward way to break it down is as follows.

getBy: For most elements this is the query you want to use. It’s very straightforward and quickly goes to the dom and looks for the specified element.


 render()
 const someNode = screen.getByTestId('some-test-id');

findBy: If you need to use await waitFor a getBy query, use findBy, as it is asynchronous and will automatically retry if it doesn’t find it the first time.


 render()
//Instead of
 const someNode = await waitFor(() => screen.getByTestId('some-test-id');
// Use
 const someNode = await screen.findByTestId('some-test-id')

queryBy: This is what you want to use only if you expect not to find an element. Queryby is unique in that it can return a null value, whereas the other two will return errors.


 render()
//looking for node that shouldn't be there
 expect(screen.queryByTestId('shouldnt-be-on-screen')).not.toBeOnScreen();

Instead of throwing an error, queryBy returns null if it doesn’t find the element, making it perfect for verifying that elements aren’t rendered. Both findBy and getBy would throw an error and cause the test to fail.

Don’t have multiple assertions in a waitFor.

If you are using waitFor for some of your assertions like if an element exists or a function was called several times, don’t put them all in a single waitFor call. There’s a reason for this. If one of the assertions fails, then you will have to wait for the waitFor to timeout before seeing the error. It may not look the best, but, if you separate your assertions, you can catch errors or failures faster, saving development time.

 await waitFor (() => {
  expect(window.fetch).toHaveBeenCalledWith('foo')
  expect(window.fetch).toHaveBeenCalledTimes(1)
}

In the above example, you can see two expect calls. If either fails, you will have to wait for the block to time out before being notified of the failure. Separate them, and the system will inform you faster if something fails.

Don’t export functions from render and use screen instead.

A common practice I have seen and done myself is destructuring functions like waitFor, findBy, and other queries from the render call of your test and using those to make assertions. There’s a better and more maintainable way to get the functions you need, and that is to import screen from the library. Screen gives you access to all the same functions you would destructure and with it. You don’t have to maintain a list of all the things you need to destructure keeping the test code looking clean and readable. And if you ever need to use a different function, you can just use screen. and not have to remember to go back and destructure it.


 const { getByRole } = render()
 const errorMessageNode = getByRole('alert')
  // Instead write it like this
 render()
 const errorMessageNode = screen.getByRole('alert')

Bonus Tip: Use the linter!

A lot of pain and headache can be saved if you install and use the eslint-plugin-testing-library package. If you are not using good patterns, choosing the wrong query, or just even using something incorrectly it will flag it for you before you get too far.

Those are my top tips for anyone using the React Testing Library package for their unit tests. I hope they help you avoid some major pain points I have definitely, probably, 100% absolutely run into. And, if you are interested in learning more about best practices when using the library, visit this page Common mistakes with React Testing Library written by the creator of the package going into more detail on these tips and more. Happy test writing!

Conversation

Join the conversation

Your email address will not be published. Required fields are marked *