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-testing-library, 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. But 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, the tutorial is not a complete introduction to 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 twist 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 has value 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 and we’ll focus on mocking GET requests in Axios.

TIP: Axios is one of the most popular library for making AJAX requests. The Fetch API is a valid, native alternative to Axios, even though it lacks some feature. These days I still prefer Axios over Fetch because of its testability. At the time of this writing Cypress still does not support stubbing for Fetch unless you use a polyfill.

Mocking Axios with Jest

Again, let’s start with a test.

Suppose we want a Users component for fetching and displaying a list of users.

A naive implementation could use async/await in componentDidMount (although it’s a bad idea).

That means when laying down our test we can await on componentDidMount.

Here’s our test’s skeleton:

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

describe("Users component", () => {
  it("shows a list of users", async () => {
    const component = create(<Users />);
    const instance = component.getInstance();
    await instance.componentDidMount();
    //
  });
});

You should be already familiar with create and getInstance from react-test-renderer.

Notice also how you can use async/await in Jest.

You probably saw something similar in a previous post of mine, when I covered testing Koa with Jest and Supertest.

Also worth noting in the above example: I’m calling componentDidMount on the testing instance.

Now if I run npm test the above test fails because I haven’t created the component yet.

Better we make a minimal implementation for it!

It could be a stateful component which gets data from an remote API with Axios, and then displays such data to our users:

import React, { Component } from "react";
import axios from "axios";

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

  handleGet(response) {
    this.setState(this.stateUpdater(response));
  }

  stateUpdater(response) {
    return { data: response.data };
  }

  async componentDidMount() {
    const response = await axios.get(
      "https://jsonplaceholder.typicode.com/users"
    );
    this.handleGet(response);
  }

  render() {
    return (
      <ul>
        {this.state.data.map(user => (
          <li key={user.name}>{user.name}</li>
        ))}
      </ul>
    );
  }
}

Nothing new uh?

I couldn’t resist using async/await on componentDidMount but let’s be clear: it is bad practice. You should always move asynchronous logic out of React components.

Anyway for this example it’s ok, we won’t do any harm.

I must confess I can’t resist to console.log too.

Are you ok if I snitch inside the state of my testing instance?

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

describe("Users component", () => {
  it("shows a list of users", async () => {
    const component = create(<Users />);
    const instance = component.getInstance();
    await instance.componentDidMount();
    console.log(instance.state) // << HERE IS THE SNITCH!
  });
});

Surprise! After running instance.componentDidMount() I can see the new state being updated!

Test-Driven React: The Definitive Guide to Testing React Components. Calling the real API with Jest

There’s also what appears to be some JSON coming from jsonplaceholder.typicode.com.

We called the real API!

Is that ok? In short, no. You should never, ever call a real API while testing.

If the API slow down your automated tests will suffer and break.

If the API goes down your tests will break too.

If the API is paid you’ll be charged every time you run npm test.

Ok, I’m sold Valentino! I really want to mock axios! Now what?

Jest makes mocking Axios easy as a breeze.

For mocking axios.get we should:

import axios inside our test

use jest.mock for mocking the axios module

provide a custom response for axios.get

use mockResolvedValue for faking the asynchronous response

Let’s do it:

import React from "react";
import { create } from "react-test-renderer";
import Users from "../Users";
import axios from "axios";

jest.mock("axios");

describe("Users component", () => {
  it("shows a list of users", async () => {
    const response = {
      data: [{ name: "Kevin Mitnick" }, { name: "Valentino Gagliardi" }]
    };
    axios.get.mockResolvedValue(response);
    const component = create(<Users />);
    const instance = component.getInstance();
    await instance.componentDidMount();
    console.log(instance.state); // << HERE IS THE SNITCH!
  });
});

If you take a look again at instance.state after running the test you’ll see the fake data inside the state:

Test-Driven React: The Definitive Guide to Testing React Components. Mocking axios with Jest

We succeded in mocking axios!

Now we can complete our test with some assertions (and remove console.log).

We want to test that our users effectively see 2 li elements from this React component.

If you remember, when testing React components we shouldn’t care about the internal state.

We’re interested in testing what the user should see.

For making the test pass I want to search for all the li elements, asserting on the expected text content:

import React from "react";
import { create } from "react-test-renderer";
import Users from "../Users";
import axios from "axios";

jest.mock("axios");

describe("Users component", () => {
  it("shows a list of users", async () => {
    const response = {
      data: [{ name: "Kevin Mitnick" }, { name: "Valentino Gagliardi" }]
    };
    axios.get.mockResolvedValue(response);
    const component = create(<Users />);
    const instance = component.getInstance();
    await instance.componentDidMount();
    const root = component.root;
    const listOfLi = root.findAll(element => element.type === "li");
    expect(listOfLi[0].props.children).toBe("Kevin Mitnick");
    expect(listOfLi[1].props.children).toBe("Valentino Gagliardi");
  });
});

Notice the use of component.root and findAll called on the root instance.

Now the test passes! Fantastic.

What’s the takeaway from this section?

When possibile you should not call a real API in automated testing. Ideally tests should not depend on external systems. By mocking axios and providing a fake response we ensure test isolation.

But faking a response from an API has some downsides too. If I were to change the url in componentDidMount like that:

async componentDidMount() {
  const response = await axios.get(
    "https://blablabala.typicode.com/users"
  );
  this.handleGet(response);
}

what do you think it happens? The test will pass as long as I mock axios with a fake response. But I won’t see any user if I open the application in a browser. The code works in testing but fails in “production”. Mocking and faking are really useful but there are some tradeoffs.

In the next section we’ll see how to stub the API response in a functional test. Remember, functional testing drives our development.

TIP: don’t forget to return the promise from axios if you prefer using axios.get().then() in componentDidMount. Otherwise you wouldn’t be able to test it:

componentDidMount(){
   return axios.get(someurl).then(doStuff).catch(handleErr)
}

TIP: doing async calls in componentDidMount is considered bad practice. Consider always moving async logic out of React components. A good place for this kind of thing 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 from axios.get. The way we do that is by mocking axios. 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?

(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. Speaking of libraries I try to use Enzyme (and shallow rendering) as less as possible. And you should too. Kent C. Dodds has the same opinion on the matter.

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!