Testing React with Jest, Enzyme, and Sinon
In this article we'll look at how to get up and running with testing React in a create-react-app. We'll look at how to configure your tests and tackle 3 common testing patterns.
Config
create-react-app comes with Jest pre-configured, but there are a few great libraries you'll want to add. The libraries you'll want are:
- enzyme: Made by AirBnb, specifically made to help test React components
- enzyme-adapter-react-16: Needed to have enzyme work with your version of React
- enzyme-to-json: Let's you customize how Jest serializes snapshots
- sinon: Spy functions, mocks, and stubs
After adding these packages, in a file called src/setupTests.js
you can configure your Jest tests:
import React from "react";import Enzyme, { shallow, render, mount } from "enzyme";import Adapter from "enzyme-adapter-react-16";import { createSerializer } from "enzyme-to-json";import sinon from "sinon";// Set the default serializer for Jest to be the from enzyme-to-json// This produces an easier to read (for humans) serialized format.expect.addSnapshotSerializer(createSerializer({ mode: "deep" }));// React 16 Enzyme adapterEnzyme.configure({ adapter: new Adapter() });// Define globals to cut down on imports in test filesglobal.React = React;global.shallow = shallow;global.render = render;global.mount = mount;global.sinon = sinon;
What we're testing
The component we'll be testing in this article is one named Temperature
. It displays some data and handles a click event which calls a function prop.
import React from "react";import PropTypes from "prop-types";import {TemperatureBox, // wraps the content inside the Temperature componentLargeText, // The top larger text inside this boxSmallText // The bottom smaller text inside this box} from "../elements/box";export default class Temperature extends React.Component {static propTypes = {temp: PropTypes.number.isRequired,city: PropTypes.string.isRequired,toggleForecast: PropTypes.func.isRequired};render() {return (<TemperatureBox onClick={this.props.toggleForecast}><LargeText>{this.props.temp}°c</LargeText><SmallText>{this.props.city}</SmallText></TemperatureBox>);}}
Snapshot testing
Inside of Temperature.test.jsx
we'll write our first snapshot test. A snapshot test is like its name implies, a "snapshot" of your component for a specific state at a specific time. It allows you to know 2 things: Does my component render without errors? And does my component render what I expect it to?
In the snapshot test below, we'll use the shallow
function from enzyme. Enzyme comes with 3 different "levels" of these functions, each providing slightly different functionality:
render
: Just give me the HTML rendered output of my component. Useful when you just care about the HTML.shallow
: Give me a light-weight representation of what my component rendersmount
: Give me a full DOM representation of my component, allowing for full traversal, events, etc... useful for testing DOM events such as onClick, onSubmit, etc...
The first time we call expect(wrapper).toMatchSnapshot();
, if a snapshot does not exist, it will create one inside of the __snapshots__
folder within the same directory you are testing in.
import Temperature from "./Temperature";it("renders correctly", () => {const wrapper = shallow(<Temperature temp={10} city="Toronto" toggleForecast={() => {}} />);expect(wrapper).toMatchSnapshot();});
This test above produces the following snapshot:
exports[`renders correctly 1`] = `<Styled(styled.div)onClick={[Function]}><LargeText>10°c</LargeText><SmallText>Toronto</SmallText></Styled(styled.div)>`;
If we run a similar test but use the render
function from enzyme, it only changes slightly:
it("renders correctly again", () => {const wrapper = render(<Temperature temp={10} city="Toronto" toggleForecast={() => {}} />);expect(wrapper).toMatchSnapshot();});
But the snapshot that is produced likes vastly different, allowing us to see the difference between shallow
and render
:
exports[`renders correctly again 1`] = `<divclass="sc-bwzfXH ilYHYa sc-bdVaJa dQdFv"><spanclass="sc-ifAKCX XBTtJ">10°c</span><spanclass="sc-EHOje exztiK">Toronto</span></div>`;
Value testing with Enzyme
If we are more interested in grabbing a specific piece of a larger component, say to ensure text is being formatted correctly, we can mount
our component and use enzyme to traverse the DOM and extract a specific value. The find
function used in the example below works similar to CSS selectors.
it("formats temp correctly", () => {// mount our Temperature componentconst wrapper = mount(<Temperature temp={10} city="Toronto" toggleForecast={() => {}} />);// extract the text from the LargeText styled componentconst text = wrapper.find("LargeText").text();// ensure it matches what is expectedexpect(text).toEqual("10°c");});
Event testing with Sinon
Lastly we can ensure that events are working as expected, by triggering a click event with enzyme, and have it call a spy function provided by sinon. Spy functions are fake functions that give us some extra functionality, namely to ask it questions like: were you called once? were you passed these arguments? how many times were you called? etc...
Because enzyme selectors always return an array of elements, even when only 1 is matched, we'll just get the first one, allowing us to simulate a click event on it.
it("calls toggleForecast on click", () => {// create a spy functionconst spy = sinon.spy();// pass spy function as our toggleForecast propconst wrapper = mount(<Temperature temp={10} city="Toronto" toggleForecast={spy} />);// find the first div and simulate a click event on itwrapper.find("div").first().simulate("click");// ensure that our spy (toggleForecast) was called when click was simulatedexpect(spy.calledOnce).toBe(true);});
Conclusion
And there you have it! We have looked at how to do some basic testing of our React components using Jest, Enzyme, and Sinon. We looked at how to configure them and then how to perform 3 different types of tests: Snapshot, Value testing, and Spy testing.
The final code used in this demo can be found here