Testing React Components: The Mostly Definitive Guide (2019)

A living, breathing guide to testing React components. Learn how to test React with react-test-renderer, React Act API, Cypress, and more.

Testing React Components: The Mostly Definitive Guide

Testing React Components: who this guide is for and what you need to know

If you have a basic understanding of React and want to get your feet wet with testing components then this guide is for you. Before starting I suggest you take some time for investigating the theory around testing frameworks and Test-Driven development.

In this guide I assume you know what Jest is and what a testing framework does, but if you need an introduction read first Jest Tutorial for Beginners: Getting Started With Jest for JavaScript Testing.

Testing React Components: what you will learn

In this guide you’ll learn:

  • unit testing React components
  • functional testing within React applications
  • the available tooling for testing React

And now enjoy the reading!

Testing React Components: getting to know snapshot testing

Snapshots are a common theme in technology: you can take a snapshot of a virtual private server for example. A snapshot is like a picture of an entity at a given point in time. And guess what is one of the simplest way for testing React components.

With snapshot testing you can take a picture of a React component and then compare the original against another snapshot later on. Snapshot testing is not specific to React, some people use snapshots for testing JavaScript code (an approach I’m not fan of). Snapshot testing is a feature built into the Jest test runner and since it’s the default library for testing React we’ll make use of it.

To start off create a new React project with create-react-app:

npx create-react-app testing-react-tutorial

While you’re there install react-test-renderer:

npm i react-test-renderer --save-dev

Next up create a new folder named tests, inside your project’s src folder (Jest will look there for new tests to run):

cd testing-react-tutorial && mkdir -p src/__tests__

When creating new components usually I start with a test. Let’s test a simple button. For our first snapshot test we need to import React, react-test-renderer, and the component to test. Create a new file in src/tests/Button.spec.js with the following test:

import React from "react";
import { create } from "react-test-renderer";

describe("Button component", () => {
  test("Matches the snapshot", () => {
    const button = create(<Button />);
    expect(button.toJSON()).toMatchSnapshot();
  });
});

At this point you can run the first pass with:

npm test

and you’ll see the test fail because there is no button component. You can create a minimal implementation of the component in the same file:

import React from "react";
import { create } from "react-test-renderer";

function Button(props) {
  return <button>Nothing to do for now</button>;
}

describe("Button component", () => {
  test("Matches the snapshot", () => {
    const button = create(<Button />);
    expect(button.toJSON()).toMatchSnapshot();
  });
});

Now run again:

npm test

And watch the test pass. Great job! But let’s break down things a little bit.

Testing React Components: demystifying snapshot testing

What is react-test-renderer? It’is a library for rendering React components to pure JavaScript objects. And what is “create”? create is a method from react-test-renderer for “mounting” the component. It’s not a “real mount”, think instead of a virtual component instance on which you can make assertions.

It is also worth noting that react-test-renderer does not use the real DOM. When you mount a component with it you’re interacting with a pure JavaScript representation of the React component. Moving further into our test there is toMatchSnapshot, here’s the code again:

import React from "react";
import { create } from "react-test-renderer";

function Button(props) {
  return <button>Nothing to do for now</button>;
}

describe("Button component", () => {
  test("Matches the snapshot", () => {
    const button = create(<Button />);
    expect(button.toJSON()).toMatchSnapshot();
  });
});

toMatchSnapshot works this way:

  • create a snapshot of the component if there isn’t any
  • check if the component matches the saved snapshot

You can also see how I called toJSON() on the component’s instance.

In other words Jest (the test runner) takes a snapshot of the component on the first run, then it checks if the saved snapshot matches the actual component. At this point one of the most frequent question is: how to choose between snapshot testing and other types of testing in React?

I have one rule of thumb: does your component changes often? If so, avoid snapshot testing. If you take a snapshot of a component the test passes but what happens when you modify the component (give it a try)? The test will fail. And you’ll have to delete the snapshot. It doesn’t sound like a big deal but …

As you may guess snapshot tests are good for components not changing so often. Put it another way: consider writing a snapshot test when you’re sure the component is stabilized.

And now let’s take a closer look at react-test-renderer!

Testing React Components: testing the wrong way (hands on react-test-renderer)

Suppose you have a button component in your application and the button should change its text from “SUBSCRIBE TO BASIC” to “PROCEED TO CHECKOUT” when clicked.

It appears the component has logic, could have a state too and that means a snapshot test would not be our best choice. react-test-renderer is a library for rendering React components to pure JavaScript objects but it can do a lot more than creating objects. In fact we can use react-test-renderer even for asserting the behaviour of our components.

Let’s create a fresh new test in src/tests/Button.spec.js:

import React from "react";
import { create } from "react-test-renderer";

describe("Button component", () => {
  test("it shows the expected text when clicked (testing the wrong way!)", () => {
    //
  });
});

As you can see I called the test “testing the wrong way”. Why so? Since we’re going to test a stateful component (a React component with its own state) we are naturally tempted to test the internal implementation of it. Let’s see. We’ll create an instance of the component with create and we’ll get that instance for asserting on its internal state.

NOTE: Since React hooks there would be no need to use ES2015 classes for using a component’s state. I wrote this article before hooks came out and I don’t want to ditch classes here. Later in the article you’ll find the hook equivalent of our button component.

I’m expecting the state text property to be empty for now:

import React from "react";
import { create } from "react-test-renderer";

describe("Button component", () => {
  test("it shows the expected text when clicked (testing the wrong way!)", () => {
    const component = create(<Button text="SUBSCRIBE TO BASIC" />);
    const instance = component.getInstance();
    expect(instance.state.text).toBe("");
  });
});

The test now is going to fail because I haven’t created the component yet. Let’s make a minimal implementation for the button component (in the same test file for convenience):

import React from "react";
import { create } from "react-test-renderer";

class Button extends React.Component {
  constructor(props) {
    super(props);
    this.state = { text: "" };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(() => {
      return { text: "PROCEED TO CHECKOUT" };
    });
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.text || this.props.text}
      </button>
    );
  }
}

describe("Button component", () => {
  test("it shows the expected text when clicked (testing the wrong way!)", () => {
    const component = create(<Button text="SUBSCRIBE TO BASIC" />);
    const instance = component.getInstance();
    expect(instance.state.text).toBe("");
  });
});

Run the test again and see how it passes. But up until this point I haven’t tested everything yet: how about handleClick? How can I test internal methods on my React components? Turns out we can call methods on our instance. Let’s update our test:

import React from "react";
import { create } from "react-test-renderer";

class Button extends React.Component {
  constructor(props) {
    super(props);
    this.state = { text: "" };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(() => {
      return { text: "PROCEED TO CHECKOUT" };
    });
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.text || this.props.text}
      </button>
    );
  }
}

describe("Button component", () => {
  test("it shows the expected text when clicked (testing the wrong way!)", () => {
    const component = create(<Button text="SUBSCRIBE TO BASIC" />);
    const instance = component.getInstance();
    expect(instance.state.text).toBe("");
    instance.handleClick();
    expect(instance.state.text).toBe("PROCEED TO CHECKOUT");
  });
});

Notice how I call:

instance.handleClick();

and then I assert that the state of the component changes as expected:

expect(instance.state.text).toBe("PROCEED TO CHECKOUT");

If I run the test again it still passes. But can you see the trap in this test? Are we testing the component from the user’s point of view? I have no clue whether my button will display the correct text to my users. I’m just asserting on it’s internal state. And that’s wrong. Let’s fix it.

Testing React Components: testing React components the right way

Testing the internal implementation of an object is always a bad idea. This holds true for React, JavaScript, and for any programming language out there. What we can do instead is testing the component by keeping in mind what the user should see. When building user interfaces the development process is driven (I really hope for you) by a functional test.

A functional test, or End to End test is a way of testing web applications from the user’s perspective.

(There is a lot of confusion and overlap among testing terminology. I suggest doing some research on your own for learning more about the various types of testing. For the scope of this guide functional testing === end to end testing).

For functional testing I love Cypress. But for now we can obtain the same result at the unit level with react-test-renderer. Let’s see how to refactor our test. We tested the internal implementation of the component, calling handleClick directly.

We bypassed completely the onClick props of our button:

      <button onClick={this.handleClick}>
        {this.state.text || this.props.text}
      </button>

And we didn’t test what the user should see. Can we do better? Turns out we can use .root instead of .getInstance() in our test. According to the documentation testRenderer.root “returns the root test instance object that is useful for making assertions about specific nodes in the tree”. Let’s find our button then! We can say:

const button = instance.findByType("button");

And from there I can access the button props:

button.props.onClick();
button.props.children;

Here’s the new test:

import React from "react";
import { create } from "react-test-renderer";

class Button extends React.Component {
  constructor(props) {
    super(props);
    this.state = { text: "" };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(() => {
      return { text: "PROCEED TO CHECKOUT" };
    });
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.text || this.props.text}
      </button>
    );
  }
}

describe("Button component", () => {
  test("it shows the expected text when clicked (testing the wrong way!)", () => {
    const component = create(<Button text="SUBSCRIBE TO BASIC" />);
    const instance = component.root;
    const button = instance.findByType("button");
    button.props.onClick();
    expect(button.props.children).toBe("PROCEED TO CHECKOUT");
  });
});

How does it look? Way better than the previous test, when we tested component’s state. Granted, we could also simulate the click event manually (we’ll see that later). But for now we’re good: we tested what the user should see and not the internal component’s state like we did before.

Always remember: do not test the implementation. Test the component from the user’s perspective. In other words: test what the user should see.

Well done. In the next section we’re going to see what testing means when using React hooks.

React hooks interlude: the Act API

Until React hooks there was only one way for keeping local state inside a React component: ES2015 classes. Classes are verbose and less intuitive than a JavaScript function. They won’t die anytime soon (imagine how much React components were written with classes) but with hooks we can slim down our components a lot. The Button component we created above can become way more tiny with hooks:

import React, { useState } from "react";

function Button(props) {
  const [text, setText] = useState("");
  function handleClick() {
    setText("PROCEED TO CHECKOUT");
  }
  return <button onClick={handleClick}>{text || props.text}</button>;
}

Now how about testing this component? Here’s the test:

import React, { useState } from "react";
import { create } from "react-test-renderer";

function Button(props) {
  const [text, setText] = useState("");
  function handleClick() {
    setText("PROCEED TO CHECKOUT");
  }
  return <button onClick={handleClick}>{text || props.text}</button>;
}

describe("Button component", () => {
  test("it shows the expected text when clicked", () => {
    const component = create(<Button text="SUBSCRIBE TO BASIC" />);
    const instance = component.root;
    const button = instance.findByType("button");
    button.props.onClick();
    expect(button.props.children).toBe("PROCEED TO CHECKOUT");
  });
});

Run it with:

npm test

and look at the output:

    Warning: An update to Button inside a test was not wrapped in act(...).

    When testing, code that causes React state updates should be wrapped into act(...):

    act(() => {
      /* fire events that update state */
    });
    /* assert on the output */

Interesting. Our test does not work with React hooks. Turns out we need to use a new testing API for React called Act. But in contrast to a react-test-renderer test, the Act API involves calling ReactDOM. Now our component will behave like it’s actually mounted in the DOM (Document Object Model). For doing that we need to tweak a bit our test.

Still in src/tests/Button.spec.js wipe everything out and start by initializing the new test:

import React, { useState } from "react";
import ReactDOM from "react-dom";
import { act } from "react-dom/test-utils";

let container;

beforeEach(() => {
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  document.body.removeChild(container);
  container = null;
});

// test here

We import a couple of helpers and more important we initialize a minimal DOM structure for our component. Next up we can create the actual test. Step 1, mount the component with Act:

import React, { useState } from "react";
import ReactDOM from "react-dom";
import { act } from "react-dom/test-utils";

let container;

beforeEach(() => {
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  document.body.removeChild(container);
  container = null;
});

function Button(props) {
  const [text, setText] = useState("");
  function handleClick() {
    setText("PROCEED TO CHECKOUT");
  }
  return <button onClick={handleClick}>{text || props.text}</button>;
}

describe("Button component", () => {
  test("it shows the expected text when clicked", () => {
    act(() => {
      ReactDOM.render(<Button text="SUBSCRIBE TO BASIC" />, container);
    });
    // more soon
  });
});

Step 2, once mounted make assertions on it, like:

    const button = container.getElementsByTagName("button")[0];
    expect(button.textContent).toBe("SUBSCRIBE TO BASIC");

You can also fire events on the button. Here’s the complete test (it will pass without any problem):

import React, { useState } from "react";
import ReactDOM from "react-dom";
import { act } from "react-dom/test-utils";

let container;

beforeEach(() => {
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  document.body.removeChild(container);
  container = null;
});

function Button(props) {
  const [text, setText] = useState("");
  function handleClick() {
    setText("PROCEED TO CHECKOUT");
  }
  return <button onClick={handleClick}>{text || props.text}</button>;
}

describe("Button component", () => {
  test("it shows the expected text when clicked", () => {
    act(() => {
      ReactDOM.render(<Button text="SUBSCRIBE TO BASIC" />, container);
    });
    const button = container.getElementsByTagName("button")[0];
    expect(button.textContent).toBe("SUBSCRIBE TO BASIC");
    act(() => {
      button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    });
    expect(button.textContent).toBe("PROCEED TO CHECKOUT");
  });
});

Seems a bit of boilerplate, especially calling all these DOM methods. Library like react-testing-library can help but learning the Act API is valuable too.

And now hands on mocking!

Testing React Components: mocking interlude

Fetching and displaying data is one of the most common use cases for a front-end library. This is usually done by contacting an external API which holds some JSON for us. In React you will use the componentDidMount lifecycle method for making an AJAX call as soon the component mounts. (And with hooks there is useEffect).

Now, the thing is: how do you test an AJAX call within React? Should you make a call to the actual API? Maybe! But some questions arise. Consider this: your team runs automated testing in a CI/CD environment, developers commit to the main branch 3/4 times a day.

What happens if the API goes down? The tests will fail with no reason at all. And what happens if every call to the API costs money? Clearly, contacting the real API during testing is far from optimal. So? What’s the solution? We can use fakes and mocks.

Faking external requirements is a common pattern in testing. For example we can replace an external system with a fake during the testing phase. Since the advent of modern testing frameworks we can even mock functions.

Mocking is the act of replacing an actual function with a fake copy. In the next section we’ll mock an API call in Jest.

Should I use the Act API or react-test-renderer?

Since the Act API came out I’m not using react-test-renderer anymore if not for simple snapshot tests.

The Act API helps testing React components as they were running in the real DOM, thus making possible to assert on the component with native DOM methods.

Mocking Fetch API call with Jest

Again, let’s start with a test. Suppose we want a Users component for fetching and displaying a list of users. In our test we can mount the component and then assert on the output. Let’s give it a shot by preparing a test with the Act API, this time we’ll use unmountComponentAtNode from ReactDOM for cleaning up the test properly:

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";

let container;

beforeEach(() => {
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

describe("User component", () => {
  test("it shows a list of users", () => {
    act(() => {
      render(<Users />, container);
    });
  });
});

Run the test with npm test and see it fail. And now let’s make a minimal implementation of the React component. Here’s the class component (you can easily refactor the component to hooks):

import React, { Component } from "react";

export default class Users extends Component {
  constructor(props) {
    super(props);
    this.state = { data: [] };
  }

  componentDidMount() {
    fetch("https://jsonplaceholder.typicode.com/users")
      .then(response => {
        // make sure to check for errors
        return response.json();
      })
      .then(json => {
        this.setState(() => {
          return { data: json };
        });
      });
  }
  render() {
    return (
      <ul>
        {this.state.data.map(user => (
          <li key={user.name}>{user.name}</li>
        ))}
      </ul>
    );
  }
}

Import the component in your test file (I called it Users.spec.js) and let’s start by making a simple assertion on the container’s text content:

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import Users from "../Users";

let container;

beforeEach(() => {
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

describe("User component", () => {
  test("it shows a list of users", () => {
    act(() => {
      render(<Users />, container);
    });
    expect(container.textContent).toBe("What to expect?");
  });
});

Run the test with npm test and that’s what you get:

 FAIL  src/__tests__/Users.spec.js
  User component
    ✕ it shows a list of users (30ms)

  ● User component › it shows a list of users

    expect(received).toBe(expected) // Object.is equality

    Expected: "What to expect?"
    Received: ""

A failing test! There is no text content in the container. That makes sense because componentDidMount is calling Fetch which is asynchronous. With React 16.9 the Act API gained the ability to deal with asynchronous functions, and that means we can await on the component’s rendering like so:

describe("User component", () => {
  test("it shows a list of users", async () => {
    await act(async () => {
      render(<Users />, container);
    });
    expect(container.textContent).toBe("What to expect?");
  });
});

Be aware that Act cannot wait for componentDidMount and the real API endpoint is not getting hit. The test still fails with the same error, no text content inside the container.

That’s by design according to Facebook developers. Act won’t wait for the real API. In the end I think it’s a sane default because most of the times there is no reason to call external APIs during testing.

But its not that bad because we’re interested in mocking the API with a fake JSON response. That’s done with jest.spyOn. First we can provide a fake response and while we’re there let’s adjust the assertion (what follows is just the relevant part of the test suite):

describe("User component", () => {
  test("it shows a list of users", async () => {
    const fakeResponse = [{ name: "John Doe" }, { name: "Kevin Mitnick" }];

    await act(async () => {
      render(<Users />, container);
    });

    expect(container.textContent).toBe("John DoeKevin Mitnick");

  });
});

Next up we mock the actual Fetch API with jest.spyOn:

describe("User component", () => {
  test("it shows a list of users", async () => {
    const fakeResponse = [{ name: "John Doe" }, { name: "Kevin Mitnick" }];

    jest.spyOn(window, "fetch").mockImplementation(() => {
      const fetchResponse = {
        json: () => Promise.resolve(fakeResponse)
      };
      return Promise.resolve(fetchResponse);
    });

    await act(async () => {
      render(<Users />, container);
    });

    expect(container.textContent).toBe("John DoeKevin Mitnick");

    window.fetch.mockRestore();
  });
});

You may find this bit of code extremely hard to reason about, especially if you’re just starting out with testing:

    jest.spyOn(window, "fetch").mockImplementation(() => {
      const fetchResponse = {
        json: () => Promise.resolve(fakeResponse)
      };
      return Promise.resolve(fetchResponse);
    });

Worry not, here’s how it works. jest.spyOn “spies” on the Fetch method, available on the window object. When the method is called we mock, aka replace the real Fetch with a so called mock implementation (.mockImplementation). mockImplementation takes a function which is our fake Fetch.

TIP: check out Fetch API: Building a Fetch Polyfill From Scratch if you want to learn how Fetch works internally.

Inside the mock we create a new response object with a function called json. This function returns a resolved Promise with the fake JSON response. Finally we return the entire response object inside another resolved Promise. And that’s it! Now run the test again with npm test and see it passing! Fantastic. Main takeaways from this section?

Do not call a real API in your tests. Ideally tests should not depend on external systems. By mocking Fetch and providing a fake response we ensure test isolation. In the next section we’ll see how to stub the API response in a functional test. Remember, functional testing drives our development.

TIP: doing async calls in componentDidMount is poor practice for bigger projects. Consider always moving async logic out of React components. A good place for async logic is a Redux middleware.

Stubbing out responses with Cypress

In this guide we started straight away with unit tests but I should have done the other way around. In Double-loop TDD it’s functional testing that drives our development. We write a functional test for testing that our application satisfy some user stories. When the functional test fails we move to write unit and integration tests which in turn drive how we code our components.

It’s no secret that I love Cypress for functional testing. In this section I’ll show you how to stub out a response from an external API.

In the previous section we faked the response for Fetch. The way we do that is by mocking the real Fetch. That is, mocking means replacing a real function with a fake. Stubbing is a bit different. When I say stubbing out responses it means switching off the real API. How we do that in Cypress?

NOTE: At the time of this writing Cypress still does not support stubbing for Fetch unless using a polyfill. For making the following example work you can replace Fetch with Axios inside your Users component.

(Before moving on I suggest looking at my Cypress tutorial for learning about the syntax in Cypress).

Suppose I want to write a little functional test for my component. It could start like so:

describe("Some APP", () => {
  it("as a user I can see a list of people", () => {
    cy.visit("/");
    cy.contains("Valentino Gagliardi");
  });
});

This test will fail straight away because my React component is calling the real API:

Testing React Components: stubbing out responses with Cypress

Cypress is smart enough to give me an hint: notice XHR (XMLHttpRequest) in the picture. How can I avoid calling the real API?

Cypress gives you two powerful tools. All I have to do is calling cy.server and cy.route before visiting the page:

describe("Some APP", () => {
  it("as a user I can see a list of people", () => {
    cy.server();
    cy.route({
      method: "GET",
      url: "**/users",
      response: [{ name: "Valentino Gagliardi" }]
    });

    cy.visit("/");
    cy.contains("Valentino Gagliardi");
  });
});

Notice how I can stub out the response with cy.route. If I run the test again it passes:

Testing React Components: stubbing out responses with Cypress

Cypress gives you again a clear indication in the test’s detail: XHR Stub means that we’re not interacting with the real service. There’s even a “routes” section which gives you info about the routes we stubbed out. Neat!

Now, this looks really clever but as with mocking, stubbing out responses does not give any indication whether the app will work in production. Mocking and stubbing are great but I suggest using them with caution.

RESOURCES: if you want to learn more about Double-loop TDD look no further than Obey the testing goat by Harry Percival (it’s for Python but the concepts are the same fo every programming language). For learning about stubbing in Cypress check out the official doc: Network Requests. If you’re confused about mocking and stubbing check out What is the difference between a stub, a mock and a virtual service?

Conclusions

Some time ago I asked on Reddit: “What’s the consensus among the React community for testing React components?” Shawn Wang replied: “testing is an enormously complicated and nuanced topic on which there isn’t a consensus anywhere in JS, much less in React.” I was not trying to find the best library for testing React. Mainly because there isn’t one.

But there’s one truth, as we saw in the previous sections: use a library that encourages best practices and do not test the internal implementation, even if Enzyme or react-test-renderer make it easy. Here are my tools of choice for testing React apps:

  • react-test-renderer for snapshot unit testing
  • Act API for unit testing React components
  • Jest for unit and integration testing of JavaScript code
  • Cypress for end to end / ui testing

I also suggest taking a look at React testing library, a nice wrapper around the Act API.

I hope you learned something new from this article but at this point you might asking: why testing our React components? Why testing our applications? Everything is great until you play with React on your own but as soon as you hit the real world you’ll be in trouble.

I’m focusing on React here but really, it could be any other library or framework. As Jacob Kaplan-Moss said: “Code without tests is broken by design”. I bet it is! The problem is that most courses and tutorials don’t give a **** about testing. If you learned the basics of React don’t stop there. Begin asking yourself questions:

  • how to test React and Redux within a project?
  • what are acceptance tests? Do I know something about double-loop TDD?

and you’ll be on the road to become a great developer. In this post you learned:

  • how to unit test React components
  • functional testing, mocking, and stubbing
  • testing tools

Make sure to come back from time to time, the guide is constantly updated.

Thanks for reading and stay tuned!