Jest, The React.js Unit Testing Framework, In Practice
Since writing this post, we have greatly revised our opinion about Jest. Learn more on Jest Through Practice.
Jest is the de facto unit testing framework for ReactJS project. It is provided and used by Facebook themselves. But it's not a library as easy as mocha for instance. Here is the story of our Jest usage on a real world project, react-admin
.
Why Using Jest
I read many blog posts and comments that started with the question "Why not using Jest?", but I decided to start with "Why using Jest?". First, Facebook engineers develop and maintain this testing framework. It's supposed to be stable an performant. And it it's really close to the ReactJS philosophy. It's simple, standard and standalone (based on JSDom and build on top of Jasmine).
Top features are:
- Automatically finds tests
- Automatically mocks dependencies
- Runs your tests with a fake DOM implementation
- Runs tests in parallel processes
Unfortunately, in practice all those features need some tweaks to be usable and efficient. I decided to invest some time, and I wanted to share my experience here.
Project Context
The project where we first used Jest is react-admin. It's a clone of ng-admin, but built on top of ReactJs. It uses a component called admin-config, which contains all business objects to be able to configure an administration. So react-admin iss only about the interface, it's made of React components, routes, and it uses the Flux architecture.
We needed to heavily test our components and unit tests are perfect candidates. For global testing, we also added functional tests, using Protractor, but it's not the topic of this post. We currently use the latest version of ReactJS (0.13.*) and we write JavaScript with the new ES2015 syntax (using the babel compiler - see our blog post about Webpack + React setup for more details).
First Contact With Jest
Installation starts with node install jest-cli
. Once Jest was installed, we wrote our very first test:
jest.dontMock("../Datagrid.js");
var React = require("react/addons");
var Datagrid = require("../Datagrid.js");
var TestUtils = React.addons.TestUtils;
describe("Datagrid", function() {
it("should set header with correct label for each field, plus an empty header for actions", function() {
var fields = {
id: {
label: function() {
return "#";
},
},
title: {
label: function() {
return "Title";
},
},
created_at: {
label: function() {
return "Creation date";
},
},
};
var datagrid = TestUtils.renderIntoDocument(<Datagrid fields={fields} />);
datagrid = React.findDOMNode(datagrid);
var headers = [].slice
.call(datagrid.querySelectorAll("thead th"))
.map(h => h.textContent);
expect(headers).toEqual(["#", "Title", "Creation date", ""]);
});
});
First run, and boom, Node fatal error...
The reason: Jest is only compatible with Node 0.10. At Marmelab, we love to use latest versions as much as possible, so I was running on the latest Node (0.12). After some research, we understood why Jest is not compatible with the 0.12 version of Node, and I wanted to give you my opinion about this fact. Many, too many, people complain about it, directly on the Jest project (see the related issue). This is NOT an issue with Jest, but the well know fragmentation of the NodeJs community since IoJs introduction. JSDom, used by Jest, is now only compatible with IoJs, and Jest wanted to keep compatibility with Node.
Anyway, let's switch to node 0.10 and accept using a (very) old version of JSDom and we can start unit test our components, which is our main goal.
Dealing With The Auto Mock Feature
In theory, auto mocking is a great feature. However we don't really need/want to use many mocks in our tests. The strategy is to disable auto mocking and set mock on some modules, explicitly. But it's not as easy as just setting jest.autoMockOff()
on top of your test files!
Here are the rules to follow to make it works:
- You must write the
require('my_module')
declaration in thedescribe()
section of your test. - You can't use the new ES2015
import
feature. - You can't use
config.rootDir
orconfig.testPathDirs
, otherwise Jest will do auto mocking...
Using ES2015 Syntax in Your Tests and Dependencies
To be able to use the new JavaScript syntax, you will need to add a preprocessor before executing your test. Thanks to the config.scriptPreprocessor
, you can easily configure it. We first chooe babel-jest, but this preprocessor just ignored all dependencies. We needed to compile our admin-config
dependency to be able to use it in our tests.
So we decided to write our own preprocessor script, loosely based on babel-jest
.
Here is the script we wrote:
var babel = require("babel-core");
module.exports = {
process: function(src, filename) {
if (
(filename.indexOf("node_modules") === -1 ||
filename.indexOf("admin-config")) &&
babel.canCompile(filename)
) {
return babel.transform(src, {
filename: filename,
stage: 1,
retainLines: true,
compact: false,
}).code;
}
return src;
},
};
Testing Speed
Once Jest tests started working well, our tests suite quickly grew up. But, you will think there is always a "but", running tests became very, very slow. It's not always a pleasure to write tests, and waiting 10 or 20 seconds to execute a single test and more than 5 min for the entire (small) tests suite, is a nightmare.
Let's optimize this. Remember, one of the top feature of Jest is that tests are running in parallel processes. It's a great idea for tests, but not so great for preprocesssing! The preprocess hook compiles all required files for each test on the fly, and parallel processes are not optimized for stuffs like that.
I decided to remove the preprocessing hook, and do the compilation before running tests. The execution time of the entire test suite (including compilation step) decreased from 5 minutes to less than 1 minute. As for tests in isolation, our configuration helps to decrease the execution time by a similar factor (example: from 8.5 seconds to 2.5 seconds). It's huge!
Here is the part of our makefile
that runs unit tests:
test-unit-init:
./node_modules/babel/bin/babel/index.js app --out-dir src --stage 1 --compact false > /dev/null
test-unit-clean:
rm -rf ./src
test-unit-run:
@./node_modules/jest-cli/bin/jest.js src
test-unit: test-unit-init test-unit-run test-unit-clean
We added a postinstall
script to npm for compiling dependencies just after installation:
postinstall:
mv node_modules/react-medium-editor node_modules/react-medium-editor_es6
./node_modules/babel/bin/babel/index.js node_modules/react-medium-editor_es6 --out-dir node_modules/react-medium-editor --stage 1 --compact false > /dev/null
mv node_modules/admin-config node_modules/admin-config_es6
./node_modules/babel/bin/babel/index.js node_modules/admin-config_es6 --out-dir node_modules/admin-config --stage 1 --compact false > /dev/null
rm -rf node_modules/react-medium-editor_es6
rm -rf node_modules/admin-config_es6
Digging Deeper
During the first days, most of our time was spent on writing test effectively. Once we were able to run Jest with confidence, we were able to make some experimentations. That's the fun part, and it's great for the quality of tests.
Dealing with react-router
ReactJS provides only the view layer. Most React applications use an additional component to handle the routing logic. In react-admin, we decided to use react-router.
In unit tests, we needed to add a router context to the tested components to be able to test them. We used a router wrapper, as explained in this blog post.
And now, let's see how to test a component using the Link
component. We wrote a mock that allows us to ensure that the Link
will go to the right route after clicking on it:
const React = require("react/react");
class Link extends React.Component {
constructor() {
super();
this.state = { clickedTo: "" };
}
click(e) {
e.preventDefault();
// Unable to read state from outside (in the test for instance)
// So we pass params in props
this.setState({
clickedTo: this.props.to,
params: JSON.stringify(this.props.params),
query: JSON.stringify(this.props.query),
});
}
render() {
return (
<a
className={this.props.className}
data-click-to={this.state.clickedTo}
data-params={this.state.params}
data-query={this.state.query}
onClick={this.click.bind(this)}
>
{this.props.children}
</a>
);
}
}
export default Link;
With this mock, we can check values of link attributes:
it("Should click on the link", () => {
const params = { entity: "MyEntity" };
const myLink = TestUtils.renderIntoDocument(
<Link className={className} to="my_action" params={params}>
Label
</Link>
);
const myLinkNode = React.findDOMNode(myLink);
expect(myLinkNode.attributes["data-click-to"].value).toEqual("");
TestUtils.Simulate.click(myLinkNode);
expect(myLinkNode.attributes["data-click-to"].value).toEqual("my_action");
const extractedParams = JSON.parse(
myLinkNode.attributes["data-params"].value
);
expect(extractedParams.entity).toEqual("MyEntity");
});
Note that we use the TestUtils.Simulate.click()
method, which is not related to Jest. ReactJs laone also provide many utility tools for unit tests.
Dealing With External Components
For complex form widgets like autocomplete dropdown or date picker, we rely on open-source third-party components. But when testing our wrapper, we don't want to test the third-party components. That's when the Jest mocking API starts to shine.
This is how we unit test such a component, which relies on the react-select
component:
jest.autoMockOff();
jest.mock("react-select", jest.genMockFromModule("react-select"));
describe("SelectField", () => {
const React = require("react/addons");
const TestUtils = React.addons.TestUtils;
const SelectField = require("../SelectField");
const Select = require("react-select");
let values = {};
const onChange = (name, value) => {
values[name] = value;
};
beforeEach(() => {
Select.mockClear();
});
it("should get a select with correct props and state", () => {
const choices = [
{ value: 1, label: "First choice" },
{ value: 2, label: "Second choice" },
{ value: 3, label: "Third choice" },
];
const value = 1;
const instance = TestUtils.renderIntoDocument(
<SelectField
name="my_field"
value={value}
choices={choices}
updateField={onChange}
/>
);
const select = TestUtils.findRenderedComponentWithType(instance, Select);
Select.mock.instances[0].fireChangeEvent.mockImplementation(function(
value
) {
this.props.onChange(value, value);
});
expect(select.props.name).toBe("my_field");
expect(select.props.value).toBe("1");
expect(select.props.options).toEqual([
{ value: "1", label: "First choice" },
{ value: "2", label: "Second choice" },
{ value: "3", label: "Third choice" },
]);
select.fireChangeEvent("2");
expect(values).toEqual({ my_field: "2" });
});
it("should get a multi select with correct props and state", () => {
const choices = [
{ value: 1, label: "First choice" },
{ value: 2, label: "Second choice" },
{ value: 3, label: "Third choice" },
];
const value = [2, 3];
const instance = TestUtils.renderIntoDocument(
<SelectField
name="my_field"
value={value}
multiple={true}
choices={choices}
updateField={onChange}
/>
);
const select = TestUtils.findRenderedComponentWithType(instance, Select);
Select.mock.instances[0].fireChangeEvent.mockImplementation(function(
value
) {
this.props.onChange(value, []);
});
expect(select.props.value).toBe("2,3");
select.fireChangeEvent("2,3,1");
expect(values).toEqual({ my_field: ["2", "3", "1"] });
});
});
First, we declare the external react-select
module as a mock:
jest.mock("react-select", jest.genMockFromModule("react-select"));
We are now be able to check that props
on the external component (select
) have been correctly initialized:
expect(select.props.name).toBe("my_field");
Then, we can check that an event has been transmitted from the external component to our component, by implementing the fireChangeEvent
function of the mock:
Select.mock.instances[0].fireChangeEvent.mockImplementation(function(value) {
this.props.onChange(value, []);
});
To sum up, this test only cover the code part of our component, by checking interactions with the external component.
Conclusion
I think Jest, as a unit testing framework, does the job. Since it uses a DOM implementation, it's sometimes hard to figure out where is the limit between unit tests and functional tests. With some tweaks, we managed to have acceptable performance. We are now able to write test like we want to.
Jest will, in the near future, become more robust and more up to date. NodeJS 0.12 and IoJs 2.0 will become NodeJS 3.0, and Jest plans on using the latest JSDom release.
As Jest is supported and used by Facebook, even if there are some alternatives (and I'm curious to receive some feedbacks about those), we found that it's more secure to use it.
You can browse the code in the react-admin project repository, and of course, all contributions are welcome!
I hope that this post will help you to start using Jest, or improve your testing experience with Jest. Of course all tips are welcome, this is our first usage of Jest, I'm sure we can do better!