Testing React: Jest and Enzyme - getfutureproof/fp_guides_wiki GitHub Wiki
This is a walkthrough for adding unit tests to a React project using Jest, Enzyme and, optionally, Sinon. These tools are not specific to React (except Enzyme) and there are many alternative options. create-react-app
(CRA) generated projects need less setup but your file structure may not match the one given below (you can change it if you like!)
If you are looking for a walkthrough using React Testing Library instead of Enzyme, check out our guide here.
Install Dependencies
In a CRA project, Jest is already installed and configured. For this walkthrough we will need only to run:
npm install --save-dev enzyme enzyme-adapter-react-16 sinon
If you use CRA and are interested to see what is going on behind-the-scenes, I recommend initializing a CRA project and then running npm run eject
to gain access to the scripts it is running for us
In a non-CRA project there is a little bit more setup but not too much. Check out the official configuration guide here.
1. Install Dependencies
npm install --save-dev jest babel-jest enzyme enzyme-adapter-react-16 sinon
2. Add Jest config to handle static assets (css and other files) Add config to package.json
// in package.json
{
// etc
"jest": {
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js"
}
},
// etc
}
3. Create mock files referenced in the above setup
- Create a new folder at the top level of your project called
__mocks__
\ - In it, make a file called
fileMock.js
with the contentmodule.exports="test-file-stub"
\ - Also make a file called
styleMock.js
with the contentmodule.exports={}
Create (or, in CRA, use the existing) setupTests.js
file (I've put it in a src/test
folder) and let's do some basic setup that we'll want to use across all our test files. If using CRA, clear out this file contents and start from scratch with us.
We'll start with React itself, we're sure to need that across the board!
import React from 'react';
Next let's add some very short setup which allows us to use Enzyme seamlessly with React.
// in setupTests
import { configure, shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
And finally, if you'd like to use sinon across our test suites, let's include it in our overall setup.
import sinon from 'sinon';
We'll only be using configure
and Adapter
in this file. The others we'll want to make available for the duration of our test runs, across all our test files.
global.React = React;
global.shallow = shallow;
global.sinon = sinon; // optional, if using sinon library
We want to make sure that this setup file is run each time we run our tests. CRA will do this for us is the file is at the top level of the src
folder but if you want to store it elsewhere or if you are not using CRA, you can add the location using a flag on your test script:
In CRA
// in package.json "scripts"
"test": "react-scripts test --setupFiles ./src/test/setupTests.js"
No CRA
// in package.json "scripts"
"test": "jest --watch --setupFiles ./src/test/setupTests.js"
Let's write tests for a basic App component. Jest is pretty smart and will pick up files which follow most standard test file naming conventions. We'll go with the CRA default of App.test.js. I've decided to store my tests in a folder called test
so I've moved it into there. I've also got rid of everything CRA added for me so we'll start from scratch. My file structure currently looks like this:
-/myApp
-/__mocks__
-fileMock.js
-styleMock.js
-/node_modules
-/public
-/src
-/test
-App.test.js
-setupTests.js
-App.js
-index.js
-package.json
-package-lock.json
We'll need to access whichever file holds the code we're testing.
// in App.test.js
import App from '../App.js';
This will look familiar to most people who have used almost any testing tools - we start with Jest's version of describe
:
describe('App', () => {
// test things here!
})
Next I'm going to declare a few things that I'll be referencing multiple times. We'll definately be referencing the App component throughout this file so let's declare that. We'll also be testing the behaviour of a form and an input. The danger here is that the effects of one test are left hanging over into the next. We will circumvent that by using Jest's beforeEach
method which will run before each individual test that we run.
describe('App', () => {
let component, form, nameInput;
// You will usually find `component` name `wrapper` in documentation.
beforeEach(() => {
component = shallow(<App />);
form = component.find('form');
nameInput = component.find('#nameInput');
})
})
Above we have used Enzyme's shallow
to dictate the level at which we need access to this component. render
and mount
are alternative option if you need deeper access.
Note also how Enzyme's find
method takes arguments in a similar style to document.querySelector
: (('tagName')
, ('#id')
or ('.class')
)
In a describe
block, multiple test
s can be defined:
describe('App', () => {
beforeEach(() => {
// some pre-test setup
});
test("what test 1 tests for", () => {
// how we test it
});
test("what test 2 tests for", () => {
// how we test it
});
})
Once we have used find
to grab an element, we can access its text content with .text()
. Let's check that our <h1>
has the word 'News' in it. I'm not fussed about if there are other words so I'll use the assertion of .toContain
. .toBe
and .toEqual
are possible alternatives here depending on how specific you want to be.
test("has 'News' in the title", () => {
expect(component.find('h1').text()).toContain("News");
});
Simulating an event couldn't be easier thanks to Enzyme's handy simulate
method!
component.find('button').simulate('click');
Let's use simulate
to fake a user adding text input. I know that my handler function that I passed to my onChange
for this element receives the event e
as an argument and then accesses e.target.name
and e.target.value
. Since simulate
offers a fake event, we'll have to stub out the content of that event and pass it as the second argument.
nameInput.simulate("change", {target: {name: "nameInput", value: "B"}});
When testing a form submission, it is likely your handler function will run a preventDefault
on the incoming event. In that case we'll need to stub out that method too. You might be reusing this so consider making it accessible more widely than a single test.
const basicFakeEvent = { preventDefault: () => "do nothing" };
form.simulate("submit", basicFakeEvent);
To access state you can call directly on your variable that stores your shallow
render. Let's do that in conjuction with a change simulation, testing a controlled form input:
test("updates state when a user enters input", () => {
nameInput.simulate("change", {target: {name: "nameInput", value: "B"}});
expect(component.state('nameInput')).toBe('B');
});
You can access the props
object of any element using Enzyme's props()
method:
test("clears user input after submission", () => {
nameInput.simulate("change", {target: {name: "nameInput", value: "Beth"}})
form.simulate("submit", basicFakeEvent);
expect(component.find('#nameInput').props().value).toBe("");
});
Sometimes we wrap components for example with react-router-dom
's withRouter
or react-redux
's connect
.
This causes a bit of confusion for enzyme but it is easily remedied. When setting up for a wrapped component, just say so when doing your shallow
render by using ComponentName.WrappedComponent
:
let component
beforeEach(() => {
component = shallow(<Things.WrappedComponent />);
})
If the component you are testing is expecting props (either from trickle down or via mSTP/mDTP), you will need to pass it some fake ones for test purposes. We pass props to our test components the same way we would anywhere else:
let component;
beforeEach(() => {
const dogStub = { name: 'Mochi', age: 1 };
component = shallow(<DogCard dog={dogStub}/>);
});
We can also pass fake functions and even test to see if they have been called upon, how many times, and with what arguments
let component;
let likeDog = jest.fn();
beforeEach(() => {
const dogsStub = [{ name: 'Mochi', age: 1 }, {name: 'Masha', age: 7}];
component = shallow(<DogCard dog={dogStub} likeDog={likeDog}/>);
})
test('it calls props.likeDog when clicking on like button', () => {
const likeButton = component.find('.likeButton').first() // get the first element with a class of 'likeButton'
likeButton.simulate('click')
expect(likeDog.mock.calls.length).toBe(1) // checks how many times likeDog was called
expect(likeDog.mock.calls[0][0].toEqual('Mochi') // checks to see if likeDog was called with argument of 'Mochi'
}
Let's say we want to check to see if one of our custom methods were called when we click on an element. My app is set up so all the stories
in state are rendered as an <li>
. I'll start by manually updating state so I can be 100% certain what is in it and then grabbing the first li on the page.
component.setState({ stories: [ { id: 2503, headline: 'Disaster Strikes', snippet: 'It was a dark and stormy night...'} ] });
const story1 = component.find('li').first();
Next to set up the spy which will be checking our component's handleStorySelect
method. To access this, we will need to create an actual instance of our component before extracting the method and creating a spy.
const instance = component.instance();
const handleStorySelect = sinon.spy(instance, 'handleStorySelect');
Now we have everything we need, let's simulate clicking on the li:
story1.simulate('click');
And now we can make our assertions. Clicking on the li should have called handleStorySelect
once and given the data I manually set at the start of the test, I expect the id of 2503
to have been passed as an argument.
expect(handleStorySelect.calledOnce).toBe(true);
expect(handleStorySelect.calledWith(2503)).toBe(true);
Put it all together:
test("clicking on a story triggers a handleStorySelect function", () => {
component.setState({ stories: [ { id: 2503, headline: 'Disaster Strikes', snippet: 'It was a dark and stormy night...'} ] });
const story1 = component.find('li').first();
const handleStorySelect = sinon.spy(instance, 'handleStorySelect');
story1.simulate('click');
expect(handleStorySelect.calledOnce).toBe(true);
expect(handleStorySelect.calledWith(2503)).toBe(true);
});
When running tests, the script CRA uses (or adding the --watch
flag on a non-CRA script) will watch
which means your test suite will run on every change. The usage instructions are wonderfully clear with your most commonly used commands likely to be q
to quit watch mode, a
to re-run all the tests and f
to re-run only the failing tests.
For coverage I have set up a new script in the package.json
:
CRA Setup
"coverage": "react-scripts test --setupFiles ./src/test/setupTests.js --coverage --watchAll=false"
No CRA
"coverage": "jest --setupFiles ./src/test/setupTests.js --coverage --watchAll=false"
This can be called with npm run coverage
and will run the test suite, display the coverage and exit.