Testing React Components with Enzyme and Mocha


17 minute read


I originally wrote this as a tutorial for Semaphore CI’s community site; I am republishing my own work here :) Testing React Components with Enzyme and Mocha

Testing React Components with Enzyme and Mocha

Introduction

Even if you only dabble in the JavaScript world, you have probably heard of React. React has become an increasingly popular and widely-used JavaScript application tool for developing web applications. Popular frameworks like Angular.js, Ember.js, and Backbone have traditionally been go-to choices for front-end application development, but React came onto the scene in 2013 has provided front-end engineers with (yet) another substantial alternative. React was developed at Facebook in part as a response to what seemed to be a particularly complicated codebase. As such, it has simplicity baked into its design and API. With this simplicity comes some key differences from libraries and frameworks the same space. Despite some fundamental differences (we will cover these briefly), React applications need testing just like any other all software. We will take a quick look at React and some tools you can use to test React components.

By the end of this article, you should:

  • Have a basic understanding of React.js
  • Know how to test simple react components using Enzyme and Mocha
  • Understand different aspects of testing front-end applications

The full source code for this tutorial can be found here.

Prerequisites

  • Node.js (available here or via nvm)
  • npm (comes bundled with node)
  • access to the internet ()

React in 30 Seconds

Most significant technology is usually too broad of a topic to adequately cover in one tutorial. For that reason, we will not be completely covering React. There are many great books and other resources available online to help you go deeper with React. Before moving forward, we will look at it briefly. What is React, you ask? Simply, React is technology for building user interfaces. Here are a few of its notable characteristics:

  • UI-centric: React is not a framework and it has a fairly minimal API surface. It is “just the view” and leaves you to make your own choices about other concerns (application architecture, AJAX, etc.).
  • Static mental model: This means that you can think of the application state in a static, declarative way, rather than having to work through or keep track of a series of mutations to the state in your head.
  • Components: It is component-focused and components are the primary currency of React.
  • Virtual DOM: React implements a virtual DOM and performs updates to the real DOM in an efficient and performant way so you do not have to. You are freed up from dealing with view-binding logic and can focus on key parts of your application.
  • Performance: React’s use of a virtual DOM and other performance-conscious designs ensure most React applications are incredibly fast and memory-efficient. The virtual DOM is more about simplicity than performance, but it also plays a role in contributing to good performance.
  • JSX: You can write the markup for your components in JSX, which allows you to completely bundle your components and create familiar HTML-like view markup.
  • One-way data-flow: Data flows through React from parent to child components, meaning you do not have to deal with potentially confusing bidirectional data-flow. Components are also further decoupled from each other, as data is only passed down in place of side-effects.

We know a little bit about React, so now we can start to look at the testing tools available to us and how we might go about testing a simple React component.

Testing Tools for React

In many ways, testing front-end applications is no different from testing server-side or other types of software. Like always, there exists a spectrum of testing types and a well-tested application will be tested across the applicable types. Generally, the two “ends” of the spectrum are unit and end-to-end or system testing, with unit testing being the most “low-level” or specific and end-to-end being the most integrated and “high-level”. For a back-end application, more integrated, high-level tests might, for example, involve interacting with other applications across the network barrier or mutating data in a test database. With front-end applications, an end-to-end test will be more focused on user interaction and dealing with inputs, events, and the like. Unit-testing in front-end applications is more similar to unit-testing in general, as it focuses on the smallest units of an application and generally aims to determine correctness (“does a function return the right value and type?”).

Jest

One of the main source of challenges for testing front-end applications is sometimes the nature of advanced technologies like Angular, Ember, and React. It is not enough to simply document.getElementById('myElement'), run some assertions against it, and move on. Most modern JavaScript application technologies are doing far more advanced things than jQuerying-up otherwise-static web pages. For example, React implements a virtual DOM as one of its key components. So, ensuring that your components are rendering, receiving props and state, and updating properly means end-to-end tests need to be suited to React. Specialized testing tools often need to be created for these sorts of specialized frameworks and libraries. For example, you might have used or heard of Protractor, the end-to-end testing tool from the Angular.js team. While it can be used for a wide variety of projects, it takes special care to be oriented towards testing Angular applications. As it turns out, the “standard” tool for testing React applications (developed by the Facebook team), Jest, is still evolving along with the rest of the React ecosystem, making it a somewhat difficult tool to teach and recommend to those who want to learn about React and front-end testing. It also brings with it some features — auto-mocking — for instance, that are a little too heavy for the purposes of a tutorial and might hinder our progress while learning about testing React components.

Enzyme + Mocha

Even while not ruling Jest out as a useful tool altogether, we will move forward with what is likely to be a familiar tool to JavaScript developers — Mocha — and a well-developed new React testing library called Enzyme. Mocha is a well-known and flexible test runner that you can use to run your JavaScript tests on the server or in the browser. Enzyme, created by engineers at Airbnb, is “a JavaScript Testing utility for React that makes it easier to assert, manipulate, and traverse your React Components’ output.” Airbnb is one of the more well-known companies using React extensively and they have put together a very useful tool in Enzyme. One of the nice things about this pairing is that you can substitute your favorite testing framework for Mocha (tape, ava, Jasmine, and so on) and still be able to test your React app. Furthermore, because of the way that React-Native is currently (built with a dependency on the actual React library), you can use Enzyme to test your React Native applications, too.

Getting Set Up

To keep up with the latest ECMAScript standard, we will be writing nearly all of our code in the ES6 manner. Many aspects of the new ES6 specification do not yet have native support, so we will use the popular Babel transpilation tools to ensure our code is able to run in browsers today. We will also use Webpack to ensure everything gets properly bundled together and can run in the browser in a single file. Babel and Webpack are extremely flexible and complex tools in their own right, so we will only be covering a very basic setup and usage of them.

Git going

Before we start, make sure that you have all the needed prerequisites installed and have run git init in your directory. Feel free to change any aspect of the directory structure on your own, but the directory structure we will use will look like this:

├── LICENSE
├── README.md
├── dist
├── lib
├── package.json
├── test
└── webpack.config.js

Babel

The Babel transpilation toolset lets you turn your ES6 code to ES5. To get it working, first we will need to install some libraries:

$ npm install --save babel-register babel-core babel-loader babel-preset-airbnb babel-preset-es2015 babel-preset-react babel-preset-stage-0

Note: Feel free to refer to this package.json to make sure all your npm modules are installed or see the full list of modules used throughout this tutorial.

We can break down the modules we just installed a little further:

  • The key one is babel-core, which as you might guess is the core babel library.
  • babel-loader is a loader plugin for Webpack.
  • The rest of the libraries are presets for Babel that allow tuning and customization.

We also need to create a .babelrc dotfile in the root of your repository:

{
  "presets": ["airbnb", "es2015", "stage-0"]
}

Webpack

Now that we have Babel set up and installed some of the presets will need, we can configure Webpack. Webpack is a module bundling tool that we can use to ensure all of our different files get combined into one meaningful file we can use in the browser. As with babel, Webpack is also a tool that deserves an entire tutorial in its own right, so we will not be going through every aspect of how it works or how to configure it.

First, ensure we have installed webpack and the webpack development server:

$ npm install --save-dev webpack webpack-dev-server

Next, we need to create a configuration file for Webpack. This file is the primary way to customize and configure Webpack. It will contain options that will tell Webpack how to bundle our files, where the output should go, and other customizations.

webpack.config.js

const path = require("path");
const webpack = require("webpack");

// env
const buildDirectory = "./dist/";

module.exports = {
    entry: "./lib/main.jsx",
    devServer: {
        hot: true,
        inline: true,
        port: 7700,
        historyApiFallback: true,
    },
    resolve: {
        extensions: ["", ".js", ".jsx"],
    },
    output: {
        path: path.resolve(buildDirectory),
        filename: "app.js",
        publicPath: "http://localhost:7700/dist",
    },
    externals: {
        cheerio: "window",
        "react/lib/ExecutionEnvironment": true,
        "react/lib/ReactContext": true,
    },
    module: {
        loaders: [
            {
                test: /\.jsx?$/,
                exclude: /(node_modules|bower_components)/,
                loader: "babel",
                query: {
                    presets: ["react", "es2015", "stage-0"],
                },
            },
        ],
    },
    plugins: [],
};

A few sections are worth looking more closely at to ensure we understand the general idea of what we are telling Webpack to do.

This section is simply how we tell Webpack to create the final output of our application script. We are also telling Webpack that we will make our script publically available at http://localhost:7700/dist.

  output: {
    path: path.resolve(buildDirectory),
    filename: 'app.js',
    publicPath: 'http://localhost:7700/dist',
  },

This part of webpack.config.js will configure webpack-dev-server, which is an extremely helpful tool that will serve up and help recompile our script for us as we work.

  devServer: {
    hot: true,
    inline: true,
    port: 7700,
    historyApiFallback: true,
  },

The last part of the webpack configuration we will spend some time on is externals. These will help enable Enzyme to work properly.

externals: {
    'cheerio': 'window',
    'react/lib/ExecutionEnvironment': true,
    'react/lib/ReactContext': true,
  },

Creating Our Index.html

We need to create a simple index.html file that we can open in a browser to see what our component looks like. This file will only do a few things: include our script, provide a render target, and include some boilerplate css.

index.html

<html>
    <head>
        <title>Fun with react testing!</title>
        <meta charset="utf-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, minimum-scale=1.0" />
        <meta name="author" content="Mark Thomas" />
        <link
            rel="stylesheet"
            href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
        />
    </head>

    <body>
        <div id="root">
            <h1>Loading!</h1>
        </div>
        <script src="http://localhost:7700/dist/app.js"></script>
    </body>
</html>

After creating this file, your project directory should look like this:

├── LICENSE
├── README.md
├── dist
├── index.html
├── lib
├── package.json
├── test
└── webpack.config.js

Mocha and Company

The last setup we need to do before we are ready to start writing our tests is install mocha and some other modules to ensure we have everything ready for it to run our Enzyme tests.

We will install rest of testing tools: mocha, jsdom, and react-addons-test-utils. Enzyme needs react-addons-test-utils and jsdom for some of its functionality in the way we will be using it.

$ npm install --save-dev enzyme mocha jsdom react-addons-test-utils

Lastly, we will create a setup file that will ensure we can test our components in a realistic browser environment using jsdom.

test/helpers/browser.js1

require("babel-register")();

var jsdom = require("jsdom").jsdom;

var exposedProperties = ["window", "navigator", "document"];

global.document = jsdom("");
global.window = document.defaultView;
Object.keys(document.defaultView).forEach((property) => {
    if (typeof global[property] === "undefined") {
        exposedProperties.push(property);
        global[property] = document.defaultView[property];
    }
});

global.navigator = {
    userAgent: "node.js",
};

documentRef = document;

Our setup is now complete. We can run our tests with npm test and run our dev server with npm run dev:hot by adding these scripts to our package.json:

  "scripts": {
    "test": "mocha -w test/helpers/browser.js test/*.spec.js",
    "dev:hot": "webpack-dev-server --hot --inline --progress --colors --watch --display-error-details --display-cached --content-base ./"
  },

When you run npm run dev:hot, you will see Webpack bundling up all your assets together into different chunks. Any errors in importing or exporting data will cause it to throw an error and report it to you in the terminal.

Testing Components

Now that we have completely finished setting up our tools, we can get to writing some tests. We will be creating a very simple component that will grab a profile image from Gravatar when a user puts their email in and clicks a “fetch” button. Our tests will need to reflect these external requirements and will serve as a guide once we start building the actual components. One great thing about letting tests guide our development is that when all of our tests pass, we can be fairly confident in our code and we know we are done.

Our main component will have two sub-components that we want to test individually. We will create one for the avatar image that will be displayed and one for the email input. If we were working on a full React application, we might find other places to reuse and repurpose these components or find they already existed in some form — this is one of the many great things about React.

This is the test for the avatar component. avatar.spec.js

import React from "react";
import { mount, shallow } from "enzyme";
import { expect } from "chai";

import Avatar from "../lib/avatar";

describe("<Avatar/>", function () {
    it("should have an image to display the gravatar", function () {
        const wrapper = shallow(<Avatar />);
        expect(wrapper.find("img")).to.have.length(1);
    });

    it("should have props for email and src", function () {
        const wrapper = shallow(<Avatar />);
        expect(wrapper.props().email).to.be.defined;
        expect(wrapper.props().src).to.be.defined;
    });
});

Take note of this line, which you will see repeated in one form or another in our other tests:

const wrapper = shallow(<Avatar />);

The shallow method from Enzyme will allow us to “shallowly” render a component. This type of rendering is used to isolate one component for testing and ensure child components do not affect assertions. You can think of it as rendering “just” the component you want it to. Enzyme gives us several ways to render components for testing: using shallow, mount, and static. We have already discussed shallow rendering. Mount is “real” rendering that will actually render your component into a browser environment. If you are creating full React components (and not just stateless components), you will want to use mount to do testing on the lifecycle methods of your component. We are using jsdom to accomplish rendering in a browser-like environment, but you could just as easily run it in a browser of your choosing.

The last major top-level rendering method enzyme gives us static, which is used for analyzing the actual HTML output of a component and will not be used in our tests. Both shallow and mount return wrappers that give us many helpful methods we can use to find child components, check props, set state, and perform other testing tasks. We will chai’s expect assertion-style on these methods in our tests.

Now that we know a little bit more about how to use enzyme, we can finish up our tests:

email.spec.js This is the spec for the email component

import React from "react";
import { mount, shallow } from "enzyme";
import { expect } from "chai";

import Email from "../lib/email";

describe("<Email>", function () {
    it("should have an input for the email", function () {
        const wrapper = shallow(<Email />);
        expect(wrapper.find("input")).to.have.length(1);
    });

    it("should have a button", function () {
        const wrapper = shallow(<Email />);
        expect(wrapper.find("button")).to.have.length(1);
    });

    it("should have props for handleEmailChange and fetchGravatar", function () {
        const wrapper = shallow(<Email />);
        expect(wrapper.props().handleEmailChange).to.be.defined;
        expect(wrapper.props().fetchGravatar).to.be.defined;
    });
});

Lastly, we need to create tests for the main component that will utilize our other two components. Gravatar’s API uses an MD5 hash to obscure email addresses, so we will use the md5 module to create a hash of the email.

gravatar.spec.js This is the main component we are building and should contain the other two components we will create

import React from "react";
import { mount, shallow } from "enzyme";
import { expect } from "chai";
import md5 from "md5";

import Gravatar from "../lib/gravatar";
import Avatar from "../lib/avatar";
import Email from "../lib/email";

describe("<Gravatar />", () => {
    it("contains an <Avatar/> component", function () {
        const wrapper = mount(<Gravatar />);
        expect(wrapper.find(Avatar)).to.have.length(1);
    });

    it("contains an <Email/> component", function () {
        const wrapper = mount(<Gravatar />);
        expect(wrapper.find(Email)).to.have.length(1);
    });

    it("should have an initial email state", function () {
        const wrapper = mount(<Gravatar />);
        expect(wrapper.state().email).to.equal("[email protected]");
    });

    it("should have an initial src state", function () {
        const wrapper = mount(<Gravatar />);
        expect(wrapper.state().src).to.equal("http://placehold.it/200x200");
    });

    it("should update the src state on clicking fetch", function () {
        const wrapper = mount(<Gravatar />);
        wrapper.setState({ email: "[email protected]" });
        wrapper.find("button").simulate("click");
        expect(wrapper.state("email")).to.equal("[email protected]");
        expect(wrapper.state("src")).to.equal(
            `http://gravatar.com/avatar/${md5("[email protected]")}?s=200`
        );
    });
});

Wrapping Up Our Tests

Once you have finished writing your tests, your project directory should look something like this:

├── LICENSE
├── README.md
├── dist
├── index.html
├── lib
├── package.json
├── test
│   ├── avatar.spec.js
│   ├── email.spec.js
│   ├── gravatar.spec.js
│   └── helpers
│       └── browser.js
└── webpack.config.js

Testing User Interactions

Now that we have our tests set up and can run them via npm test, we can work on building out our component. First, we need to create our sub-components: avatar and email.

First, we can create the avatar component:

avatar.jsx

import React, { PropTypes } from "react";
export default class Avatar extends React.Component {
    render() {
        return (
            <div className="avatar">
                <p>
                    <em>{this.props.email}</em>
                </p>
                <img src={this.props.src} className="img-rounded" />
            </div>
        );
    }
}

Avatar.propTypes = {
    email: PropTypes.string,
    src: PropTypes.string,
};

So far, so good. We are not creating anything terribly advanced and even if you are brand new to React you can rely on the standard JavaScript semantics to infer what there is to a React component. Next, we will create our email component.

Email.jsx

import React, {PropTypes} from 'react';
export default class Email extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <div className="form-group">
        <input onChange={this.props.handleEmailChange} className="form-control" style={{
          width: 200
        }} type="text"/>
        <button onClick={this.props.fetchGravatar} className="btn-success btn ">Fetch</button>
      </div>
    );
  }

Email.propTypes = {
  handleEmailChange: PropTypes.func,
  fetchGravatar: PropTypes.func,
};

Lastly, we will create our main <Gravatar/> component that makes use of the others we have created so far. It also uses of a few simple functions to listen for state changes and user click events. Note that these functions are passed as props to our child components, keeping the view logic contained in our parent component.

Gravatar.jsx

import React, { propTypes } from "react";
import md5 from "md5";

import Avatar from "./avatar";
import Email from "./email";

export default class Gravatar extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            email: "[email protected]",
            src: "http://placehold.it/200x200",
        };
    }

    updateGravatar() {
        this.setState({
            src: `http://gravatar.com/avatar/${md5(this.state.email)}?s=200`,
        });
    }

    updateEmail(event) {
        this.setState({ email: event.target.value });
    }

    render() {
        return (
            <div className="react-gravatar">
                <h4>Avatar for:</h4>
                <Avatar email={this.state.email} src={this.state.src} />
                <Email
                    fetchGravatar={this.updateGravatar.bind(this)}
                    handleEmailChange={this.updateEmail.bind(this)}
                />
            </div>
        );
    }
}

If you run your tests again using npm test, you should get green results. Our component is relatively small, but even at this scale you should be able to notice that Enzyme is a fairly speedy tool for testing components. Often, end-to-end tests or any that require a browser-like environment can be slow to spin up. While full integration tests will still likely be somewhat slower, Enzyme’s shallow rendering allows for speedy unit-testing of components.

Wrapping Up Creating Components

At this point, your directory should be looking something like the following:

├── LICENSE
├── README.md
├── dist
├── index.html
├── lib
│   ├── avatar.jsx
│   ├── email.jsx
│   ├── gravatar.jsx
│   └── main.jsx
├── package.json
├── test
│   ├── avatar.spec.js
│   ├── email.spec.js
│   ├── gravatar.spec.js
│   └── helpers
│       └── browser.js
└── webpack.config.js

Summary

We have worked through creating a simple component that will display the gravatar image for a user based on the email they put in. We looked at testing some static properties of components, hierarchy and existence of children, as well as simulating some more integrational events like user clicks. Hopefully, your appetite for writing elegant, testable React applications has been whetted and you are on your way to becoming a master of writing elegant, testable code applications.


Related: