On speeding up our frontend tests by 70%

March 06, 2023

Like most modern react frontends, we use React Testing Library. It lets us write a tonne of integration tests by querying and interacting with the DOM. However, late last year, we started noticing some flakiness in our CI. Tests were failing at random by exceeding the 5 second timeout imposed by Jest.

The quick fix was of course to temporarily increase this timeout to allow us to keep shipping. But, with most temporary things, we knew this would come back to bite us if we didn’t look into what was going on. So during some downtime, I took a closer look and found a compromise to significantly speed up our tests.

The Problem

As recommended by React Testing Library, we reach for byRole queries as a first priority. This ensures our app remains accessible without having to put in much more effort. If for some reason we cannot use byRole our next preference is byText and finally byTestId. After profiling our tests however, we discovered that byRole queries are by far the slowest part of the test. In fact, they can be significantly slower than other query types.

Execution time of various queries vs Number of Elements

This chart shows how long it takes to select one element as the number of rendered elements grows. As we can see, the execution time for byText and byTestId remain relatively constant as the number of elements grows. The problem however is that byRole queries grow linearly.

This was an issue for us since the pages where tests were failing were large settings-type pages with a tonne of elements. Some of our tests were taking multiple seconds to select a single element which all adds up when running in-band in CI.

The Compromise

We didn’t want to stop using byRole selectors. Quicker CI time vs an accessible application wasn’t a worthwhile tradeoff for us. Instead, we experimented with limiting the search space for our byRole queries.

To do this, we first select a container where we expect the element to be using a byTestId query. Once we have this container, we can use the within utility to restrict the search space for our byRole query and still get all of its advantages.

This is how we might go about improving the speed of a query on our settings page. Here we’re trying to select a particular checkbox to later assert its state or interact with it.

import { screen, within } from "@testing-library/react"

// 🐌
const devModeCheckbox = screen.getByRole("checkbox", {
  name: /developer mode/i,
})

// 🚀
const devModeCheckbox = within(
  screen.getByTestId("advanced-options-section")
).getByRole("checkbox", { name: /developer mode/i })

Results

Our new technique to limit the search space for byRole queries involves minimal rework and gives us a huge increase in performance.

Execution time of our new query technique vs Number of Elements

Apart from the obvious outlier at position 1, we can see a massive difference between the original byRole query and our new technique. We’re steadily converting our tests to use this new technique and are extremely happy about our CI times decreasing with every push.


Profile picture

Personal blog of Aquib Master (Keeb)