Testing React Components with react-test-renderer, and the Act API

Learn how to test React components with react-test-renderer, and the Act API.

Testing React Components

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.

This guide assumes a basic understanding of testing theory and testing runners, like Jest.

For an introduction to Jest check out 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 for React applications
  • the available tooling for testing React

Enjoy the reading!

Disclaimer

Starting from version 3.3.0 create-react-app uses react-testing-library as the default testing library.

I wrote this guide before react-testing-library was a thing: it focuses mostly on react-test-renderer/Act API which are a bit more low level.

Testing React components: getting to know snapshot testing

Snapshots are a common theme in technology. A snapshot is like a picture of an entity at a given point in time. Guess what is also 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 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

Move inside the project folder and install react-test-renderer:

cd testing-react-tutorial && 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):

mkdir -p src/__tests__

Most of the times when creating a new React component I start off by creating a test for it. For those uninitiated this practice is called test-driven development and you don't have to follow it literally.

It's fine to start off coding without any test, especially when the idea of what implementation you'll write is not yet formed in your mind. Anyway for the scope of this tutorial we'll practice a bit of TDD with a test for a simple button component.

TIP: you can get method autocomplete for Jest by installing its type definitions with:

npm i @types/jest --save-dev

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 a first pass with:

npm test

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.

Demystifying snapshot testing

At this point you might be asking what is react-test-renderer? react-test-renderer is a library for rendering React components to pure JavaScript objects, while create is a method from react-test-renderer for "mounting" the component.

It's worth noting that react-test-renderer does not use the real DOM. When you mount a component with react-test-renderer you’re interacting with a pure JavaScript object, a 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() does all the heavy lifting under the hood. It:

  • creates a snapshot of the component if there isn’t any
  • checks if the component matches the saved snapshot

You can also see toJSON() called 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 you may wonder how to choose between snapshot 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 on the first run but as soon as there is a change the test will fail because there'll be a mismatch between the component and its original "picture".

As you may guess snapshot tests are good for components that don't change often. Put it another way: write a snapshot test when the component is stable.

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.

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 and make assertions on the internal state our component.

NOTE: With React hooks there is no need to use classes for holding component's state. I wrote this article before hooks came out and I don't want to ditch classes here. Later in the post 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. Up until this point I haven't tested anything 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 if my button will display the correct text to my users. I'm just asserting on its internal state. Let's fix that.

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

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");

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. Granted, we could also simulate the click event manually (we'll see that later). 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 a 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.

JavaScript's classes are great for programmers coming from languages like Java and C#, but they are verbose and less intuitive than a JavaScript function, especially for beginners.

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.

For example the Button component we created earlier becomes 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 a React component based on hooks? Can we just reuse the original 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. "Luckily" there are two ways for writing tests with the Act API.

Let's see them.

React hooks interlude: Act API with react-test-renderer

If you can live with the fact that react-test-renderer does not use a DOM you'll need just to tweak the test a bit for Act. That means importing act alongside with create:

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

Your Button component will stay the same:

import React, { useState } from "react";
import { create, act } 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>;
}

The test must use act() for any action that changes the component's state, like "mounting" it or clicking on a function passed as a prop. Here's the complete test with Act:

import React, { useState } from "react";
import { create, act } 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", () => {
    let component;
    act(() => {
      component = create(<Button text="SUBSCRIBE TO BASIC" />);
    });
    const instance = component.root;
    const button = instance.findByType("button");
    act(() => button.props.onClick());
    expect(button.props.children).toBe("PROCEED TO CHECKOUT");
  });
});

Notice that both the call to create and to button.props.onClick() are wrapped in a callback passed to act(). That's it if you don't need the DOM.

If instead you want to mount React components into the Document Object Model then another version of the Act API will suite you best.

Head over the next section!

React hooks interlude: Act API with the real DOM

Since Act came out I'm using it almost exclusively. I pick react-test-renderer only for writing snapshot tests. The approach we're going to see is my favourite because tests for me feel more real if I can interact with the DOM.

The Act API is available both on react-test-renderer and on react-dom/test-utils and when imported from the latter it's possible to use ReactDOM.render, thus mounting the React component into the Document Object Model.

Still in src/__tests__/Button.spec.js wipe everything out and start with a 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 act from react-dom/test-utils, ReactDOM, 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 you can make assertions on the DOM which now contains your component:

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

You can also fire DOM 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");
  });
});

NOTE: button.dispatchEvent seems to have effect even if the call is not wrapped inside act.

Seems a bit of boilerplate, especially all these DOM methods. It's not a big deal for me but libraries like react-testing-library can help on this.

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 can use componentDidMount for making AJAX calls as soon as 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 CI/CD, developers commit to the main branch 3/4 times a day.

What happens if the API goes down? The tests will fail with no reasons. Clearly, contacting the real endpoint in 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 in testing.

Mocking Fetch API calls with Jest

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

Again, let's start with a test (act API on ReactDOM). 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.

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 mine Users.spec.js) and let's start by making a simple assertion on the container's text content:

// rest omitted for brevity

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 never hit. The test still fails with the same error, no text content inside the container.

That's by design according to Facebook developers. 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 it's not that bad because we're interested in mocking the API with a fake JSON response. 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 code extremely hard to reason about, especially if you're just starting out with testing.

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 takes a function which is our fake Fetch.

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.

Now run the test again with npm test and see it passing! Fantastic.

Takeaways:

Do not call a real API in your tests. Tests should not depend on external systems. By mocking Fetch and providing a fake response we ensure test isolation.

TIP: doing async calls in componentDidMount is poor practice. Consider always moving async logic out of React components. A good place for async logic is a Redux middleware, or a custom Hook for data fetching.

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.

Thanks for reading and stay tuned!

Valentino Gagliardi

Hi! I'm Valentino! I'm a freelance consultant with a wealth of experience in the IT industry. I spent the last years as a frontend consultant, providing advice and help, coaching and training on JavaScript, testing, and software development. Let's get in touch!