At Visa, I was writing unit tests for a Next.js project using components designed with Chakra UI. That's where React Testing Library came in handy. Unlike other solutions like Enzyme, I did not have to worry about the application snapshot but could instead focus on each UI element, its expected behaviour and the data it would render upon user interactions.
In Jest, you usually split your tests into groups using the
describe block. You can write a descriptive message as the title of the block to indicate what kind of tests you are including in this block. Similarly, each individual test case is written using the
test wrapper, with a message to describe the individual test case.
The nice thing about Jest - which can also be a pain point when you actually get down to writing the tests - is that they execute in parallel rather than sequentially. You can force sequential execution by using the
--runInBand parameter and this comes in handy sometimes while debugging issues related to timing. However, test suites are generally executed in parallel so that they run faster.
jest example.test.js --runInBand
With Jest, you write tests using the
expect clauses, where you specify the matcher that you are expecting and compare it to the received output, to verify that the expected behaviour is satisfied.
You can specify the chunks of code you wish to execute before and after each test case using the
afterEach clauses respectively. If you want to include some code before all the test cases are executed or after all the test cases are executed, you can use the
afterAll clauses. These functions are very useful for consolidating all the common data setup as well as doing some cleanup.
Mocking is a useful feature in Jest that allows you to test the expected behaviour when an API call is made without actually calling the respective APIs. This is necessary for testing because calling the actual APIs can be costly, since these calls are subject to constraints like network bandwidth. These can also be used for simulating calls to third party dependencies, which are not supposed to be tested because they have been developed by others.
These are the abstractions (functions) from the React Testing Library that I have used most for writing unit tests:
screenAPI allows you to access the HTML DOM, and print out either the entire DOM or a portion of it using the
debugmethod. You can also retrieve element(s) by using the
findBy*is an asynchronous function that allows you to fetch an HTML Element or an array of HTML Elements.
queryBy*, on the other hand, allows you to fetch either as well as return null if they are not found.
getBy*throws an error if the element(s) in question is not found.
getAllBy*are variants of the respective methods that let users retrieve an array of matching elements instead of a single element.
waitForallows you to execute a block of code and wait for it to finish before you move on to the other blocks of code in the test case.
renderis used to render a React Component or Fragment as you would in a React application. This is basically a re-export of the original render function from the React library.
fireEventallows you to handle trigger events on the different elements like
select. An alternative is the
userEventlibrary for simulating the user interactions.
Let's say we build a login form using Chakra UI. We want to do very basic validations on the form data and check the logic flow once the
submit button is called.
You only need to specify the respective test ids for the components while designing the front end. You can then match the values filled in with the expected values after carrying out the interactions. Here, the mock for the
console object comes in handy to determine whether the statement is printed upon successful login or not.
While writing unit tests seems straightforward in theory, it requires a good amount of trial and error in practice.
These are some of the issues that I faced along the way:
If the tests involve a large set of UI interactions like filling in multiple fields in a form or handling multiple input events, they are likely to time out before the whole test case executes, or even in the waitFor block. A work around is using
fakeTimers in jest and then
runAllTimers once the component is rendered so that the test timer waits for the completion of the execution. Another way, is to use
setTimeout and specify the timeout you are expecting.
The main problem is detecting the line(s) of test code responsible for the delay in test execution. For this, you have to usually resort to some trial and error with reordering the test cases in the suite or even the way you break the test cases into suites. You might also have to log the execution of the source code at several points to see whether the data obtained from a mock is the same one that the test was expecting at that point in time. The problem is compounded due to the asynchronous nature of execution of React code in hooks.
Honestly, this is a very frustrating problem to solve. The first easy step to troubleshoot this issue is to have all the shared data setup for the test cases in the
afterEach clauses. However, it could also be that some of the tests have taken longer to execute than others and therefore, only part of the source code gets tested in a failing case. Resolving this issue involves significant trial and error similar to the timeout issue.
Sometimes the element(s) you are looking for simply cannot be found. You would have to print out the whole DOM using
debug and inspect why this is not found. It's an easy fix if the matcher specified in the
expect clause is wrong, but it becomes quite troublesome if the element is not displayed because the state of the application has not been updated. This might require you to inspect the application flow more thoroughly. It's good if the test has helped you catch the bugs in code execution, since that is the main purpose behind software testing. However, it is also possible that your code is executing correctly but there is some other issue that results in this error, like a timeout issue.