Test-driven React: How To Manually Mock Components
Nov 11, 2014 • Mars Hall
Article covers React 0.12, a javascript library for building web-based user interfaces. Updated Jan 5, 2015 with an improved Rewire example.
Why is testing a React component so damn hard?
If your answer is “use Jest,” then see below, Why not Jest?.
Any given React component may include other components.
Example my-component.js, javascript + JSX:
var MyComponent = React.createClass({
render: function() {
return <div className="my-component">
<SomeOtherComponent />
</div>;
}
});
JSX transforms <SomeOtherComponent />
into a constructor call React.createElement(SomeOtherComponent)
(more about React's vDOM.) This render-time generation of renderable elements makes mocking difficult.
What we need is someway to make SomeOtherComponent
resolve to a factory for a mock component.
Modules to the Rescue
Testing components in isolation requires a module system, so that each test subject may be loaded in a clean state for each test.
Various javascript module APIs may be utilized during development: CommonJS, AMD, and the emergent ES6 Modules. To add more complexity to the choices, various build & bundling tools are available to consume modules: Require.js, Browserify, Webpack, and ES6 transpilers.
After liberal experimentation, this solution was found using CommonJS modules with Rewire to swap-out actual dependencies for mocks.
The recipe is:
- CommonJS modules
- Webpack module bundler
- Rewire via webpack-rewire
- Jasmine testing framework
Solution
Example my-component.js from above becomes modular:
var React = require("react");
var SomeOtherComponent = require("./some-other-component");
var MyComponent = React.createClass({
render: function() {
return <div className="my-component">
<SomeOtherComponent />
</div>;
}
});
module.exports = MyComponent;
With the dependency SomeOtherComponent
declared as a variable in the top-level scope of the module, Rewire may be used to swap it out for a mock.
Example rewire-module.js, a Jasmine test helper to setup & teardown a rewired module:
var rewireModule = function rewireModule(rewiredModule, varValues) {
var rewiredReverts = [];
beforeEach(function() {
var key, value, revert;
for (key in varValues) {
if (varValues.hasOwnProperty(key)) {
value = varValues[key];
revert = rewiredModule.__set__(key, value);
rewiredReverts.push(revert);
}
}
});
afterEach(function() {
rewiredReverts.forEach(function(revert) {
revert();
});
});
return rewiredModule;
};
module.exports = rewireModule;
Example my-component-spec.js:
var React = require("react/addons");
var TestUtils = React.addons.TestUtils;
var rewire = require("rewire");
var rewireModule = require("./rewire-module");
describe('MyComponent', function() {
var component;
// `rewire` instead of `require`
var MyComponent = rewire("./my-component");
// Replace the required module with a stub component.
rewireModule(MyComponent, {
SomeOtherComponent: React.createClass({
render: function() { return <div />; }
})
});
// Render the component from the rewired module.
beforeEach(function() {
component = TestUtils.renderIntoDocument( <MyComponent /> );
});
it("renders", function() {
var foundComponent = TestUtils.findRenderedDOMComponentWithClass(
component, 'my-component');
expect(foundComponent).toBeDefined();
});
});
Why not Jest?
Facebook provides an official testing tool for React: Jest, which includes both a headless command-line test runner & a javascript testing API, based on Jasmine.
Jest as a test runner is yet-another contender amongst awesome multi-browser & headless Javascript test tools like Karma & Testem.
My own attempts to use Jest failed, because it:
- Runs all specs against a virtual DOM (via JSDOM.) This prevents testing code that uses browser-native constructs like IndexedDB & Service Workers.
- Offers no support for running tests in a browser, so all those fantastic visual dev & debugging tools provided by browsers are unavailable for tests.
- Throws cryptic errors. This is a standard experience for javascript developers; doubly so, if the developer has used a Phantom.js test runner. The cryptic errors are made intolerable by the previous point: lack of options for in-browser debugging tools.
Happy Rewiring!
We burned days of time wrangling a React proof-of-concept into a neatly testable app and hope to save you frustration when developing with React.
Please tweet @marsi with any feedback.