Nikolajus Lebedenko

Resolving Common Component Testing Issues for React Applications

Jun 14

By Nikolajus Lebedenko, Lead Frontend Engineer at Kilo Health

During the lifecycle of an application, you will start encountering more and more problems while testing the components. Especially if you would delay testing till some later stage. 

A perfect example of this is verifying that a bug is fixed in a production application that was never tested.

For demo purposes, let’s imagine that we are trying to test the “Add to cart” form component, which accomplishes the following things:

  • After submitting the form, it sends an HTTP request containing the product ID from the Redux state.
  • It notifies the analytics service that an item is added to the cart.

The dependency scheme for the component would look like this:

Identifying dependencies

​Most of the issues while testing components occur because of the dependencies. The “Add to cart” form is not an exception. 

The first step to success is to understand the dependencies of a component:

Most of these dependencies will be present in the components which hold business logic. So, what actual problems will we face while testing the “Add to cart” form, and how can we resolve them?

Wrapping a component with context providers

The first problem is that our component requires a few context providers to render:

– `I18nextProvider` for `react-i18next`.

– `Provider` for `react-redux`.

It might sound obvious, but you could not even imagine how much time developers spend trying to understand why a component fails to render

If you forgot to add a context provider in the test, a component would crash with the error, which would sound like the following – `cannot read property randomProperty of undefined`.

The solution for missing providers is to create a custom render function that lists all the required providers. Later on, replace usage of `render` function from testing library to custom render implementation. This solution is suggested by the Testing Library itself. 

In the end, the custom render function would look like this:

const AppWrapper = ({ children }) => {
 return (
   <I18nextProvider i18n={i18next}>
     <StoreProvider store={store}>
const customRender = (component, options) => {
 render(component, { wrapper: AppWrapper, ...options });

Using a custom render function will reduce the amount of boilerplate code, and developers will not need to care much about wrapping components with context providers.

Building initial state for state management library

Secondly, our component requires a preloaded state with a predefined product inside. Usually, a state consists of quite complex objects. 

For example:

interface Product {
 id: string;
 sku: string;
 name: string;
 shortDescription: string;
 // ... other properties
interface PreloadedState {
 product?: Product;
 // ... other properties

Even though the “Add to cart” form needs only one property from the `Product` interface, we will need to pass an object that matches the `Product` interface. 

Imagine if the component depended on several complex objects – it would be a nightmare, and you would spend tons of time defining the initial state.

The solution for complex initial state issues is to implement a builder function that would generate an entity of your need. 

A product builder example:

type ProductBuilder = (product: Partial<Product> = {}) => Product;
const aProduct: ProductBuilder = (product) => ({
 id: randomId(),
 sku: randomSku(),
 name: randomProductName(),
 shortDescription: randomDescription(),
const randomProduct = aProduct();
const productWithSpecificName = aProduct({ name: 'My product' });
​const store = createStore(rootReducer, { product: aProduct() });

In builder functions, you may use libraries that would generate random properties such as ID, SKU, product name, etc. Using builder functions would allow it to easily generate entities for the components with a possibility to override certain properties.

Depending on concrete implementation rather than abstraction

The last problem you will encounter while testing the “Add to cart” form is depending on Axios and GTM. To be precise, that component will make real HTTP requests to the server and try to access the `window.dataLayer` object, which will be missing.

To resolve the last issue, we will need to do some refactoring and remove the direct dependency on problematic libraries. 

After refactoring, the scheme would look like this:

This way, the “Add to cart” form depends on implementations that match HTTP client and analytics service interfaces. For this reason, we are pretty flexible in creating test kit implementation for our dependencies and forwarding them using context providers. It will allow us to avoid HTTP requests or any environment-specific code. 

Also, providing a possibility to verify that integration between a component and service works as expected.

To wrap up

Even using a simple example of the “Add to cart” form, we have already faced some issues while writing component tests. 

  • Use the custom render function to wrap the component with context providers, which are required for your application.
  • Implement builder functions to get a preloaded state with a minimal amount of code.
  • Components should depend on abstractions that allow using test implementations for libraries with side effects.

Latest articles

Vitalijus Majorovas
Vagus Nerve Stimulation: How to Live Slowly in a Fast World
Aug 4

If you have spent any amount of time researching mental health, you have probably been bombarded with suggestions about vagus nerve stimulation.  It seems that it came out of nowhere – just a few months back, we didn’t know anything…

Read more
Lukas Varkalis
Git: How To Write A History Book Of Your Project
Jul 31

Let’s say that you dream of releasing a history book. That it is not an easy job, and there are strict guidelines you must follow. For starters, you cannot change the chronological order of the events. You also must make it easy to read and to understand for others. 

Read more
Justas Tamulionis
It’s Now the Year 2023, and the Robots Have Taken Over
Jul 25

Right away, I would like to ask you to spot a difference between these two values: 1.428234234995816237434112354534567 and 1.428234234995816237434112354524567

Mhm? Were you able to do so? Let’s just leave it for the user testing.

Read more

Stay in the loop

Get Kilo Health updates and open positions sent straight to your inbox