React testing with Mocha, Chai, Sinon and Gulp

I've created a repository with the steps we'll take in this tutorial. If you want to follow along or if you want to get your hands on some code right away you can get the code right here.

When I started developing with React I looked around for a good testing framework. Naturally my first discovery was Jest, which initially felt really nice with its auto mocking. BUT IT'S SO SLOW. Most tests took at least 4 seconds (each) to run. That is an eternity.

I've done a lot of development with Angular where I used Mocha, Chai and Sinon and started looking for a solution to running React tests using them. I found this article from which I drew a lot of inspiration, but I've made some adjustments of my own.

To get it working, there are three steps that need to be fixed:

  1. DOM mocking
  2. JSX transpilation
  3. Module mocking

Mocking the DOM

I created a DOM helper with jsdom using this example.

Note: jsdom above version 3 requires io.js. If you don't have io.js, use version 3.1.2 of jsdom.

// test/testdom.js
module.exports = function(markup) {  
  if (typeof document !== 'undefined') return;
  var jsdom = require('jsdom').jsdom;
  global.document = jsdom(markup || '');
  global.window = document.defaultView;
  global.navigator = {
    userAgent: 'node.js'
  };
};

At the top of every test file that needs DOM, I insert:

require('./testdom')('<html><body></body></html>');  

In order for React to know about this fake DOM, it needs to be included before React is required. If you have any other global variables you want mocked, add them in the testdom function. I've added navigator because some tests seem to complain about it.

JSX transpilation

I for one like the JSX implementation of HTML inside my React components. I also think it makes the components easier to understand at a glance. Therefore we will need to transpile our JSX before testing. This is done using a compiler. This bit of code gets run on every require.

I used the modified compiler from Hammerlab. It is a modified version of this one from Khan Academy which uses .js file extensions instead of .jsx.

// test/compiler.js
var fs = require('fs'),  
    ReactTools = require('react-tools'),
    origJs = require.extensions['.js'];

require.extensions['.js'] = function(module, filename) {  
  // optimization: external code never needs compilation.
  if (filename.indexOf('node_modules/') >= 0) {
    return (origJs || require.extensions['.js'])(module, filename);
  }
  var content = fs.readFileSync(filename, 'utf8');
  var compiled = ReactTools.transform(content, {harmony: true});
  return module._compile(compiled, filename);
};

It also enables Harmony which gives us some nice features from ECMAScript 6.

Mocking modules

To speed up and isolate our tests, we need to mock our components requires. For this we use proxyquire.

We create a super simple React component for mocking required React components in our tests.

// test/reactStub.js
var React = require('react');

module.exports = React.createClass({  
  render: function () {
    return <div />;
  }
});

Here's an example of how we use it:

// components/Page.js
var React  = require('react');  
var Header = require('./Header');

module.exports = React.createClass({  
  render: function () {
    return (
      <div className="page">
        <Header />
      </div>
    );
  }
});
// test/Page-tests.js
require('./testdom')('<html><body></body></html>');

var React      = require('react');  
var chai       = require('chai');  
var expect     = chai.expect;  
var sinon      = require('sinon');  
var proxyquire = require('proxyquire');  
var reactStub  = require('./reactStub');

chai.use(require('sinon-chai'));

describe('Page', function () {  
  var React;
  var TestUtils;
  var Page;
  var moment;
  var element;

  beforeEach(function() {
    React = require('react/addons');
    TestUtils = React.addons.TestUtils;

    Page = proxyquire(process.cwd() + '/components/Page.js', {
      './Header': reactStub
    });

    element = TestUtils.renderIntoDocument(
      <Page />
    );
  });

  it('should render', function () {
    expect(React.findDOMNode(element).className).to.eql('page');
  });
});

Sinon

Expanding on the uses of proxyquire, we can use Sinon for mocking third-party libraries when we want to see if function calls have been made without actually making them.

Lets say we have the same example as before, but this time we have also included moment.js to do some date calculations.

// components/Page.js
var React  = require('react');  
var Header = require('./Header');  
var moment = require('moment');

module.exports = React.createClass({  
  render: function () {
    var date = moment().format('YYYY-MM-DD');

    return (
      <div>
        <Header date={date} />
      </div>
    );
  }
});

We expand our tests we some additional information:

// test/Page-tests.js
require('./testdom')('<html><body></body></html>');

var React      = require('react');  
var chai       = require('chai');  
var expect     = chai.expect;  
var sinon      = require('sinon');  
var proxyquire = require('proxyquire');  
var reactStub  = require('./reactStub');

chai.use(require('sinon-chai'));

describe('Page', function () {  
  var React;
  var TestUtils;
  var Page;
  var moment;
  var element;

  beforeEach(function() {
    React = require('react/addons');
    TestUtils = React.addons.TestUtils;

    moment = sinon.stub().returns({
      format: sinon.spy()
    });

    Page = proxyquire(process.cwd() + '/components/Page.js', {
      './Header': reactStub,
      'moment': moment
    });

    element = TestUtils.renderIntoDocument(
      <Page />
    );
  });

  it('should render', function () {
    expect(React.findDOMNode(element).className).to.eql('page');
  });

  it('should format current date', function () {
    expect(moment().format).calledWith('YYYY-MM-DD');
  });
});

Gulp

Our Gulpfile is really simple. It includes the libraries we want and also the compiler we made earlier.

var gulp  = require('gulp');  
var mocha = require('gulp-mocha');

// Compiler for React tests
require('./test/compiler.js');

gulp.task('mocha', function() {  
  return gulp
    .src('./test/*.js', { read: false })
    .pipe(mocha());
});

gulp.task('default', [  
  'mocha'
]);

Now if we run gulp inside our project folder we should see some nice results in the console.

Conclusion

When I started testing with Jest the runtime for my tests, about 30 of the, was around 20 seconds. Using this technique the runtime has gone down to ~3-4 seconds with almost 150 tests. This is a huge difference and contributes to not loosing focus during development.

comments powered by Disqus