Testing ReactJS components with mocha-webpack

If you are using Webpack in your project, you probably want to use the bundle.js file to test your React components during the TDD cycle. The mocha-webpack package does this task easier:

npm i mocha mocha-webpack enzyme jsdom sinon chai --save-dev

We need a new file called mocha-webpack.opts in our root directory to save our mocha-webpack options:

--colors
--require ignore-styles
--require babel-core/register
--require jsdom-global/register

Our React component:

'use strict'
import { connect } from 'react-redux'
import { render } from 'react-dom'
import * as TestsActionCreators from '../actions/tests'
import React, { Component, PropTypes } from 'react'
import { Link, browserHistory, withRouter} from 'react-router'
import { Button, Modal } from 'react-bootstrap'
// export for unconnected component (for mocha tests)
export class QuestionsComponent extends Component {
constructor(props) {
super(props);
this.state = {
questions: [],
test_id: this.props.routeParams.test_id
}
}
//Test mocha: console.log('This props >>>>>>' + JSON.stringify(this.props));
}
/**
* Load test data and questions
**/
componentWillMount() {
if ( ! this.props.OneTestArrayProp.length ) {
let action = TestsActionCreators.fetchOneTest( this.props.routeParams.test_id );
this.props.dispatch(action);
}
}
render() {
return (
<div className="container_div">
{this.props.QuestionsArrayProp.map((q, i) =>
<div key={i} className="questions_div">
<div><b>{i+1}.- Question</b>: {q.question} -- {q.id} </div>
</div>
)}
</div>
{ this.props.children }
</div>
)
}
}
QuestionsComponent.propTypes = {
QuestionsArrayProp: PropTypes.array
}
QuestionsComponent.defaultProps = {
QuestionsArrayProp: []
}
const mapStateToProps = (state) => {
return {
QuestionsArrayProp: state.rootReducer.tests_rdcr.QuestionsArrayProp
}
}
export default withRouter(connect(mapStateToProps)(QuestionsComponent));

Our webpack file to be used on the test environment:

// file: webpack.testing.config.js
path = require('path');
var webpack = require('webpack');
module.exports = {
entry: {
app: ["./entry.jsx"]
},
target: 'web',
debug: 'devel',
devtool: 'eval',
output: {
path: path.resolve(__dirname, "build"),
publicPath: "/build/",
filename: "bundle.js"
},
resolve: {
extensions: ['', '.js', '.jsx']
},
module: {
loaders: [
{test: /sinon.js$/, loader: "imports?define=>false" },
{test: /\.jsx?$/, loader: 'babel', exclude: /node_modules/, query: {
cacheDirectory: true,
presets: ["react", "es2015", "stage-0"]
}, include: path.app
},
{ test: /\.css$/, loader: 'style-loader!css-loader'},
{ test: /\.js$/, loader: 'babel', exclude: /node_modules/, query: {
presets: ["es2015", "stage-0"]
}},
{ test: /\.less$/, loader: 'style!css!less' },
{ test: /\.scss$/, loader: 'style!css!sass' },
{ test: /\.json$/, loader: "json-loader" },
{ test: /\.woff(2)?(\?v=[0-9].[0-9].[0-9])?$/, loader: "url-loader?mimetype=application/font-woff" },
{ test: /\.(ttf|eot|svg)(\?v=[0-9].[0-9].[0-9])?$/, loader: "file-loader?name=[name].[ext]" },
{ test: /\.gif$/, loader: "url-loader?mimetype=image/png" },
{ test: /bootstrap-sass\/assets\/javascripts\//, loader: 'imports?jQuery=jquery' } // Bootstrap 3
]
},
externals: [
{
'isomorphic-fetch': {
root: 'isomorphic-fetch',
commonjs2: 'isomorphic-fetch',
commonjs: 'isomorphic-fetch',
amd: 'isomorphic-fetch'
},
'cheerio': 'window',
'react/addons': true,
'react/lib/ExecutionEnvironment': true,
'react/lib/ReactContext': true
}
],
node: {
fs: "empty"
},
plugins: [
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery"
}),
new webpack.IgnorePlugin(/ReactContext|react\/addons/),
new webpack.optimize.DedupePlugin()
]
};

As you probably already know, one of the mocha’s perks is that it doesn’t have a native DOM, so we must load the DOM using a helper:

// file: __tests__/helpers/browser.js
var baseDOM = '<!DOCTYPE html><html><head><meta charset="utf-8"></head><body></body></html>';
var jsdom = require('jsdom').jsdom;
global.document = jsdom(baseDOM);
global.window   = document.defaultView;
if ( global.self != null) {
console.log(' global.self >>>>> ' + global.self);
} else {
global.self = global.this;
}
global.navigator = {
userAgent: 'node.js'
};

Our mocha file to test the React class looks like this:

'use strict';
//  Mocha TEST
// file: __tests__/components/QuestionComponent.spec.js
import ReactDOM from 'react-dom';
import React from 'react';
import { expect } from 'chai';
import { mount, shallow, render } from 'enzyme';
import { QuestionsComponent } from '../../components/QuestionsComponent';
import sinon from 'sinon';
var dispatch = function() {
console.log('>>>>>>>> Mocking dispatch ');
};
var props = {
dispatch: dispatch,
routeParams: {test_id: 1},
tests_rdcr: {}
};
describe('QuestionsComponent', function () {
it('QuestionsComponent calls componentWillMount', () => {
sinon.spy(QuestionsComponent.prototype, 'componentWillMount');
const enzymeWrapper = mount(<QuestionsComponent {...props} />);
expect(QuestionsComponent.prototype.componentWillMount.calledOnce).to.equal(true);
});
it("QuestionsComponent does not render questions_div", function() {
expect(shallow(<QuestionsComponent {...props} />).contains(<div className="questions_div" />)).to.equal(false);
});
it("QuestionsComponent does not render questions_div", function() {
var answers = [{id:1, answer: 'answer uno', correct: true, active: true, question_id: 1},
{id:2, answer: 'answer dos ', correct: true, active: true, question_id: 1},
{id:3, answer: 'answer tres', correct: true, active: true, question_id: 1}
];
props['QuestionsArrayProp'] = answers;
expect(mount(<QuestionsComponent {...props} />).find('.questions_div').length).to.equal(3);
});
});

Now, one of the problems about a real-life React component is that the component is interwoven with Redux and React-Router and we need to isolate the component in order to do the Unit Testing. To achieve this action is surprisingly easy. Normally, in the production environment we import the component in this way:

import QuestionsComponent from './components/QuestionsComponent'

The lack of curly brackets around the component’s name means that we are importing the default exported element, id est:

export default withRouter(connect(mapStateToProps)(QuestionsComponent));

But if we add a non-default export statement to our React class:

// export for unconnected component (for mocha tests)
export class QuestionsComponent extends Component {

and then we import using the curly brackets:

import { QuestionsComponent } from './components/QuestionsComponent'

that means that now we can load and test the component isolated from Redux and React-Router. Pretty cool, ha?

We can now test our component:

./node_modules/mocha-webpack/bin/mocha-webpack --opts ./mocha-webpack.opts --webpack-config ./webpack.testing.config.js -r __tests__/helpers/browser.js __tests__/components/QuestionComponent.spec.js

If all was fine you must see something like: