TABLE OF CONTENTS

Effective Unit Testing of React Components

React Pagepro Picture

Intro to Unit Testing

User interface built with React is a sum of components properly working together. Most of the components are affected by props passed by parent and higher-order React components, internal state modifications, event handlers, conditional rendering.

If you want to be confident that your application will do the job in the real world, it’s good to start checking all of these factors in isolation. That’s what unit testing is all about.

Yet, there is a lot of preliminary steps to take during the React app development before starting your testing journey: planning how to structure your tests, choosing assertions matchers, picking a tool to display results and coverage reports, and providing browser environment replacement.

To succeed, we have to get it all right. At the same time, we want to avoid configuration madness, so choosing our technology wisely and plan test cases ahead is a must.

This article is divided into three parts.

  1. I will start with the description of the tools that I recommend for unit testing and present to you a contract-based approach for writing component assertions.
  2. In the next part, we will practice the process of writing the component contract.
  3. Finally, we will turn acquired knowledge into the actual unit tests code.

Step 1: Preparing the environment

Testing React components: Our technology stack

Jest

Unit testing of React Components: Using Jest

We will pick Jest as our testing framework. It’s marketed as an “out of the box” solution, shipped with the most popular boilerplate in React’s ecosystem: create-react-app. If you are bootstrapping your app with this project, most of the work will be already done for you.

If you are not using CRA, don’t worry, you can head to the short and easy-to-follow guide that describes the configuration process.

Jest provides most of the functionalities that we wanted from our testing environment: structure, matchers, displaying results and coverage reports, mocks, etc.

If you want to dig deeper into the capabilities of testing React with Jest, check out the dedicated documentation chapter.

Want to check your React components?

Enzyme

Enzyme is the second tool in our arsenal. Made by Airbnb with unit testing of React components in mind. It makes rendering, analyzing, and asserting components easy.

Enzyme will render your component and let you analyze what actually got rendered via intuitive API. It uses the same library as jQuery Core (cheeriojs) for selectors, so if you used this legendary front-end lib, you will find Enzyme quite familiar even if you never used it before.

Enzyme let us render components in three different ways:

  • Shallow – it renders only given component, without nested children
  • Full – it renders component and all of his descendants
  • Static – it renders static HTML code that is available in the browser

Shallow and static rendering doesn’t need DOM, so this kind of rendering takes a lot of less time than full rendering that actually needs some browser implementation (jsdom is the most popular one) to run.

There are mixed opinions about which rendering technique to go with for the best balance between fast execution and high reliability.

I go with a shallow rendering for Unit Testing and leave full rendering for integration tests.

This library has a rich, powerful API – I will show you 20% that gives 80% of the results. You can check the rest of the functionalities in the library docs.

To use Enzyme with Jest, we will need two packages: enzyme and enzyme-adapter-react-X (replace X with the version of React used in your project, ie. 16). Enzyme needs the adapter to provide compatibility with the given version of React API.

We have to create the first configuration file (and by the way, the last one, how cool is this?): src/setupTests.js. I recommend sticking with this file naming and path, it is automatically recognized by Jest.

Let’s start with the basic Enzyme configuration.

import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() });

We want to reduce the number of redundant imports in our test files so we will assign rendering methods as properties of node.js’s global object. This way we will be able to access them in all of our test.js files.

import Enzyme, { shallow, mount, render } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() });

global.shallow = shallow;
global.mount = mount;
global.render = render;

Let’s get formal

Defining a clear component contract makes unit testing a lot easier.

The component’s contract is nothing else but our expectations about the features provided by the component and assumptions about how it will be used in the application.

Every time you create a new component you probably have its contract loosely defined in your mind.

I found that writing component contract on paper is really useful. It improves the component’s design by exposing possible inconsistencies and, what is really important from today’s article point of view, leads to the minimal amount of unit testing code.

We want our tests to be really useful and check those parts of the component that actually matter. We don’t want to go into components internals that are prone to frequent changes.

There is some kind of symbiosis between component contract and its unit testing. Contract points us to what should be tested, tests help us to define contract more precisely. This process is all about this feedback loop, so don’t get stuck on any side of this equation.

How to create a valuable contract?

When we prepare the first draft of the component contract, there are some important factors that should be taken into consideration, no matter the given component responsibility:
props that component will receive (via the parent, hoc, redux store subscription, context etc)
state and how it will be mutated via handling user interactions lifecycle methods.

There is a lot to think about, but I found that asking those simple questions helps in pinning down the most important pieces of information:

  • What component renders?
  • What component will share with its children via props?
  • How component (and its children) interacts with the user?
  • What is actually going on in lifecycle methods?

Those four simple questions will help you steer your attention toward the most important part of every component.

If you will find yourself stuck during answering one of those questions, you have a pretty clear indication that your component is poorly designed. Probably it tries to do many things at once. Try to split it into smaller components that will be easier to test thanks to more precisely defined responsibility.

Step 2: Defining contract

We have our testing environment well-prepared. Jest and Enzyme are all we need to approach unit testing of our components effectively.

You have also learned about the component contract and its utility in the process of defining the scope of unit tests.

This time I will show you how defining the component contract looks in practice and how to represent it with actual Jest & Enzyme unit tests.

As the guinea pig, we will use container component from one of the React documentation’s example projects: Emoji Search.

You can check the app demo here.

I have made some adjustments to fit the needs of this article, so to check the source code, head to my fork on github.

The mentioned container component is called App (what a surprise!), check its code:

import React, { PureComponent } from "react";
import Header from "./Header";
import SearchInput from "./SearchInput";
import EmojiResults from "./EmojiResults";
import filterEmoji from "./filterEmoji";
import doAsyncCall from "./doAsyncCall";

class App extends PureComponent {
 constructor(props) {
   super(props);
   this.state = {
     filteredEmoji: [],
     status: null,
     maxResults: 20
   };
 }

 componentDidMount() {
   doAsyncCall().then(() => {
     this.setState({ filteredEmoji: filterEmoji("", this.state.maxResults) });
   });
 }

 handleSearchChange = event => {
   this.setState({
     filteredEmoji: filterEmoji(event.target.value, this.state.maxResults)
   });
 };

 render() {
   return (
     <div>
       <Header />
       <SearchInput textChange={this.handleSearchChange} />
       <EmojiResults emojiData={this.state.filteredEmoji} />
     </div>
   );
 }
}

export default App;

Without further ado, let’s define the contract of this component with the help of the questions that I proposed in the previous part.

Defining contract

What component render()s?

render() {
   return (
     <div>
       <Header />
       <SearchInput textChange={this.handleSearchChange} />
       <EmojiResults emojiData={this.state.filteredEmoji} />
     </div>
   );
 }

As is the case with most of the containers, the rendered content is wrapped with a div.
The app always renders three children components: Header, SearchInput and EmojiResults.

It is important to mention that we have to give special treatment to components that use conditional rendering. If you write the contract of such a component, list each branch with the consideration of the required condition and what will be rendered if it’s met.

What component shares with its children as a prop?

class App extends PureComponent {
 constructor(props) {
   super(props);
   this.state = {
     filteredEmoji: [],
     status: null,
     maxResults: 20
   };
 }

 handleSearchChange = event => {
   this.setState({
     filteredEmoji: filterEmoji(event.target.value, this.state.maxResults)
   });
 };

render() {
   return (
     <div>
       <Header />
       <SearchInput textChange={this.handleSearchChange} />
       <EmojiResults emojiData={this.state.filteredEmoji} />
     </div>
   );
 }

Let’s take an even closer look at render().

So Header is a strong, independent component. It doesn’t have any expectations data-wise from his parent.

Yet Header siblings will provide us with some room to put work in.

SearchInput is waiting for this.handleSearchChange as a textChange prop. Meanwhile, EmojiResults needs this.state.filteredEmoji passed as emojiData.

How React components (and its children) interact with the user?

App component doesn’t directly handle any events but it shares event handler logic with SearchInput.

During unit testing of this component, we will test the handleSearchChange in isolation and leave out checking communication between App and SearchInput for integration tests.

What is going on in lifecycle methods?

// App.js
componentDidMount() {
 doAsyncCall().then(() => {
   this.setState({ filteredEmoji: filterEmoji("", 20) });
 });
}

// doAsyncCall.js
function doAsyncCall() {
 return fetch("https://jsonplaceholder.typicode.com/todos/1");
}


export default doAsyncCall;

You will often initialize your state with an API call in the componentDidMount method, so I feel responsible to show you how to handle this even if this particular app loads data from a local JSON file.

I’ve moved the initialization of state.filteredEmoji to then() callback of promise returned by doAsyncCall – a method that make a request to JSONPlaceHolder API.

Part 3: Using Jest and Enzyme

So, now we have a pretty clear overview of what we should test.

We’re ready to write an actual unit test code that will reflect the contract defined above.

In the last, third part of this series, we will finally make use of Jest and Enzyme.

Preparing for a journey

Before we turn our contract into actual unit test code, let’s start with the required boilerplate code in App.test.js.

import React from "react";
import App from "./App";
import Header from "./Header";
import SearchInput from "./SearchInput";
import EmojiResults from "./EmojiResults";
import emojiList from "./emojiList.json";


describe("<App />", () => {
  let appWrapper;
  let appInstance;

  const app = (disableLifcycleMethods = false) =>
   shallow(, { disableLifcycleMethods });

  beforeEach(() => {
    appWrapper = app();
    appInstance = appWrapper.instance();
  });

  afterEach(() => {
    appWrapper = undefined;
    appInstance = undefined;
  });

  describe("", () => {
    // Unit tests code
  });
});

So, in our testing process, we will use two ways of accessing component under tests: shallow wrapper (appWrapper) and component instance (appInstance).

Shallow wrappers are used for checking static parts of our contract: what is rendered and passed props.

An instance will be useful for checking dynamic changes, i.e. how the state is affected by component logic.

Enzyme by default calls componentDidMount and componentDidUpdate lifecycle methods on shallowly rendered components. To disable this behaviour we can use disableLifecycleMethods option.

Our boilerplate take advantage of beforeEach and afterEach hooks provided by Jest. Their functionality is pretty straightforward, the code inside those blocks are called before and after each assertion.

We initialize our references before each test and set them to undefined afterwards. This way we are sure that assertions won’t affect each other.

Warming up with smoke test

We will follow unit testing tradition and start with a simple smoke test. Before taking care of the details, we want to ensure that our Component gets rendered without blowing up.

describe(“<App />”, () => {
  it("renders without crashing", () => {
    expect(app().exists()).toBe(true);
  });
});

This way we just wrote our first assertion. Every assertion that you will see fits this pattern:

expect(component).matcher(expectedValue);
  • expect(component) – with expect() we point out a specific part of component’s interface revealed by Enzyme wrapper
  • matcher – function that will compare component and expectedValue
  • expectedValue – value that we expect to be returned by expect(component)

Testing rendering

Let’s check if the component wraps everything with a div.

it("renders a div", () => {
  expect(appWrapper.first().type()).toBe("div");
});

We start with checking that the .first() element returned by App has a type() of ‘div’.

describe("the rendered div", () => {
  const div = () => appWrapper.first();

  it("contains everything else that gets rendered", () => {
    expect(div().children()).toEqual(appWrapper.children());
  });
});

Next, we have nested assertion about mentioned div inside describe block. This is a great way to increase the legibility of our test case.

To check that it wraps the rest of the content rendered by the App component, we have used an interesting feature of Enzyme. React requires us to group everything returned in render() with the root element, which usually has no other responsibility. So when you call the children() method of Enzyme’s component wrapper, this root element is ignored, and you directly access nested components/elements.

To ensure that mentioned content consists of the following components: Header, SearchInput and EmojiResults, listed in the App contract, we need simple assertions that use the find() method and toBe() matcher. This lets us check if the App renders exactly one of the given components.

it("renders <Header />", () => {
  expect(appWrapper.find(Header).length).toBe(1);
});

it("renders <SearchInput />", () => {
  expect(appWrapper.find(SearchInput).length).toBe(1);
});

it("renders <EmojiResults />", () => {
  expect(appWrapper.find(EmojiResults).length).toBe(1);
});

That would be it in terms of App rendering contract. Now, let’s move to the shared state.

Testing shared state

The header is self-reliant, so we don’t have to write additional tests for him in this section.

Meanwhile, we should check that SearchInput actually received the expected reference to handleSearchChange to textChange prop.

describe("the rendered <SearchInput />", () => {
  const searchInput = () => appWrapper.find(SearchInput);

  it("receives handleSearchChange as a textChange prop ", () => {
    expect(searchInput().prop("textChange")).toEqual(
      appInstance.handleSearchChange
    );
  });
});

This is pretty straightforward, we compare the value of prop() received by SearchInput wrapper with the value of App instance handleSearchChange method.

A similar approach lets us check that EmojiResults received filteredEmoji stored in App state as an emojiData prop.

describe("the rendered <EmojiResults />", () => {
  const emojiResults = () => appWrapper.find(EmojiResults);

  it("receives state.filteredEmoji as emojiData prop", () => {
    expect(emojiResults().prop("emojiData")).toEqual(
      appWrapper.state("filteredEmoji")
    );
  });
});

Testing user interactions

As we have checked in the previous point, App shares reference to handleSearchChange with SearchInput.

handleSearchChange = event => {
  this.setState({
    filteredEmoji: filterEmoji(event.target.value, this.state.maxResults)
  });
};

We won’t check that SearchInput uses it as we expect, that’s the scope of unit testing of SearchInput. Yet we want to ensure that this method works as intended in isolation.

We also don’t have to go into inner workings of filterEmoji used inside this method, so we will treat this method as a black box.

We will provide mocked event and check if state.filteredEmoji changes.

We will use three different scenarios that cover different kind of inputs. We will check if App meets our expectations, when event.target.value equals:

  • “” (empty string) – state.filteredEmoji should evaluate to the same length as emojiList.json.
  • “Invalid-emoji” – state.filteredEmoji should evaluate to empty array.
  • “Smile”- state.filteredEmoji should have length higher than 0 but lower than state.maxResults.

Code for each case is pretty similar, so I will only show you the most “complex” one.

it("with empty query sets state.filteredEmoji to array with state.maxResults length", () => {
  appInstance.handleSearchChange(emptyEvent);
  appInstance.forceUpdate();
  expect(appInstance.state.filteredEmoji.length).toBe(
    appInstance.state.maxResults
  );
});

Last but not least, we can proceed to lifecycle method tests.

Testing lifecycle methods

Most of the containers like App will at least make a one API call to populate its state with data stored on the server.

This project uses local json file, so for the sake of showing you how to handle async initialization, I have moved it to then() callback of doAsyncCall method that returns a promise after calling JSONPlaceholder – fake online REST API for developers.

function doAsyncCall() {
  return fetch("https://jsonplaceholder.typicode.com/todos/1");
}

export default doAsyncCall;

Even if we ignore the actual data returned from the server, we get pretty close to the real-life scenario.

Of course in our unit tests, we don’t want to rely on external API that is outside our control, wait for loading of data which could take ages.

We will take advantage of mocks, functionality that lets us provide the fake implementation of given method, doAsyncCall in this case.

To do this I created directory dedicated directory __mocks__ that will be automatically used by Jest to replace implementation during test execution.

In __mocks__ we create another doAsyncCall.js file (filenames have to be exactly the same!) and provide some simpler implementation like this:

function doAsyncCall() {
  return new Promise((res, rej) => {
    res('success')
 });
}

export default doAsyncCall;

Calling this mock will still return the promise (as we would fetch/axios api call). But this one resolves instantly with placeholder data which will make our test case fast and reliable.

To inform Jest that it should mock doAsyncCall, we have to go back to the boilerplate code (before first describe block) and insert following:

import doAsyncCall from "./doAsyncCall";

jest.mock("./doAsyncCall");

Now we can check if the state got initialized as intended:

describe("the componentDidMount lifecycle method", () => {
  it("initializes emoji state, done => {
    setTimeout(() => {
      appWrapper.update();
      const state = appInstance.state;
      expect(state.filteredEmoji.length).toBe(state.maxResults);
      done();
    });
  });
});

Wrapping code with setTimeout is crucial. This way we are sure that componentDidMount was already executed.

And that’s all folks.

Summary

In this three-step guide, we have gone through the process of preparing the testing environment, defining a component contract and writing unit testing of the stereotypical container component.

Even if our example was simple, this approach can be applied to all of the components – no matter how complex they get.

I hope that this process will ease the pain that many developers feel when they sit down to start unit testing of their components.

Article link copied

Close button