Mostly reasonable patterns for testing React on Rails
- Scope
- Constraints
- A Dirty-UMD
- Mocha vs Jasmine vs Jest
- A Mocha Setup
- Assertion Libraries
- Mock a Doc
- A Test Boilerplate
- React Shallow Rendering
- Watching the File System
This is how we write tests for React.js on Rails. We've struggled to find the happy path. This is our ongoing attempt to carve out the most direct path to testing React components on a golden-path Rails app. Recommendations here represent a good number of failed attempts. If something seems out of place, it probably is; let us know what you've found.
Our approach has the following constraints.
- Scripts work in the Asset Pipeline
- Scripts work in a JS-testing framework
- Module unit tests should work without a DOM
- Testing envirnoment should be flexible for app/team-specific needs
- ES2015 syntax support
Component definitions will need to work in window
and as a module. This is ugly but it works.
(function (global) {
"use strict";
let React;
if (typeof module === "object" && module.exports) {
React = require("react");
} else {
React = global.React;
}
class MyWidget extends React.Component {
render () {
return <div />
}
}
if (typeof module === "object" && module.exports) {
module.exports = MyWidget;
} else {
global.MyWidget = MyWidget;
}
})(this);
Feel free to DRY this out however feel right to you.
Here's how it shook out against our particular constraints.
In 2015, use Mocha. That might change if these Jest issue get resolved:
- Speedy ES2015 support
- Jasmine updated to 2.x
- Not as flexible as Mocha for app-specific needs
- Ships with DOM implementation,
jsdom
= locked to unsuportted3.x
tag - Slow. A test suite of only 4 tests and 23 assertions took 10.7 seconds.
- Locked to Jasmine 1.3
- No obvious Babel/ES2015 path
- https://babeljs.io/docs/setup/ has very obvious setups for the other frameworks.
- Flexible for teams
- Very few opinions about anything
- Well documented
- Simple to configure
- No default DOM implementation
- Fast. A test suite of only 4 tests and 23 assertions took 1.2 seconds.
Jest has very nice feature for auto-mocking and running tests in parallel. But, for now, it's an order of magnitude slower than Mocha (given our constraints).
Init package.json
, if you havent done so:
$ npm init
Install Mocha:
$ npm install mocha --save-dev
Create the test/
directory, if one doesn't exist:
$ mkdir `test/`
*mocha will automatically run tests matching ./test/*.js
. ref
Configure the npm test
command:
{
"scripts": {
"test": "mocha"
}
}
Now you can run your local version of mocha
via the npm test
command.
Additionally, you can run the local version of mocha
with flags via the node_modules
directory:
$ node_modules/.bin/mocha --watch --compilers js:babel/register --recursive --reporter nyan
Add a test/mocha.opts
for shared options:
--watch
--compilers js:babel/register
--recursive
--reporter nyan
This configuration will be used in both npm test
and node_modules/.bin/mocha
People like what they like.
My $.02. If you don't have a PM that pretends to read specs, write assertions. Just sayin'.
$ # you already have node/assert. lucky you
import assert from "assert";
assert(result.type, "div");
$ npm install expect.js --save-dev
import expect from "expect.js";
expect(result.type).to.be("div");
$ npm install should --save-dev
import expect from "should";
(result.type).should.be.exactly("div");
$ npm install chai --save-dev
Chai has 3 included assertion libraries. Chose your favorite.
import chai from "chai";
const assert = chai.assert;
const expect = chai.expect;
const should = chai.should;
assert.strictEqual(result.type, "div");
expect(result.type).should.equal("div");
result.type.should.equal("div");
API Mock a Doc
One of the criteria is testing without a DOM. That sholud be possible using React shallow rendering but there's a bug.
You can bypass it by mocking document
.
// ./test/utils/document.js
if (typeof document === 'undefined') {
global.document = {};
}
Add this flag to your mocha.opts
:
--require test/utils/document.js
Should you want a full fledged DOM, follow this guide by Jake Trent.
Here's what a standard test looks like.
"use strict";
import assert from "assert";
import React, { addons } from "react/addons";
import AppIcon from "../AppIcon.js.jsx";
let shallowRenderer = React.addons.TestUtils.createRenderer();
describe("MyWidget", () => {
shallowRenderer.render(<MyWidget />);
let result = shallowRenderer.getRenderOutput();
it("renders an div tag as its root element", () => {
assert.strictEqual(result.type, "div");
});
});
You can required your assertion library in mocha.opts
to avoid requiring in each test.
--require assert
Where reasonable, use shallow-rendering. Shallow rendering does not Require a DOM.
The ReactTestUtils-test.js file are the best documentation on how shallow rendering works.
Here are the basics:
import { addons } from "react/addons";
let shallowRenderer = React.addons.TestUtils.createRenderer();
shallowRenderer.render(<MyWidget />);
let result = shallowRenderer.getRenderOutput();
assert.strictEqual(result.type, "div");
assert.deepEqual(result.props.children, [
<div>hi!</div>,
<div>okay, bye.</div>
]);
shallowRenderer.render(<InterfacesIcon name="" hoverStyle={{ color: "blue" }} />);
result = shallowRenderer.getRenderOutput();
assert.deepEqual(result.props.style, {});
result.props.onMouseEnter();
let mouseEnterResult = shallowRenderer.getRenderOutput();
assert.deepEqual(mouseEnterResult.props.style, { color: "blue" });
*See Mock a Doc
mocha --watch
is busted. I only ran into trouble when attempting to use sinon.js.
If you use sinon
for stubs, spies, and mocks, you're going to need a dedicated fs watcher. Unfortunately, this is much slower than mocha --watch
but is has the benefit of, you know, working.
I like nodemon for filesystem watching.
$ npm install nodemon --save-dev
{
"scripts": {
"test:watch": "node_modules/.bin/nodemon -w app/assets/javascripts/interfaces/components node_modules/.bin/_mocha"
}
}
Configuration can be pulled out into nodemon.json
.
{
"watch": [
"app/assets/javascripts/interfaces/components",
"test/assets/javascripts/interfaces/components"
]
}
$ npm run test:watch
note: npm test
is first-class npm command. All other scripts must be run with the run
prefix.