Pagepro Blog

JavaScript

Effective Unit Testing of React Components (Part 3)

Posted on .

Effective Unit Testing of React Components (Part 3)

Introduction

Welcome to the last of three-part series on unit testing of React Components. This time I will show how to use Jest and Enzyme for testing of container component.

I won’t get into details that were already described, so go ahead and start with reading previous parts (if you haven’t already):

  1. Environment and process overview
  2. Preparing component contract

Preparing for a journey

Before we turn our contract into actual unit test code, let’s start with 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 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 rest of the content rendered by App component we have used an interesting feature of Enzyme. React requires us to group everything returned in render() with the root element, that usually has no other responsibility. So when you call 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 following components: Header, SearchInput and EmojiResults, listed in App contract, we need simple assertions that use find() method and toBe() matcher. This lets us check if App renders exactly one of 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

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 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.

Similar approach let 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-part series, 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.

Marcin Czarkowski

Marcin Czarkowski

There are no comments.

View Comments (0) ...
Navigation