Testing with ReactJS at Codecademy

At Codecademy, we use ReactJS to build our learning environment. We’ve really enjoyed working with React; it’s well-suited to our learning environment, which is for the most part a single-page app.

Of course, as part of our React setup, we wanted the ability to write tests for our React components! A few months ago, I started exploring our options. Spoiler alert: we settled on Karma and Jasmine, because it was over 100x faster than Jest.

The Codecademy learning environment. codecademy.com/learn/learn-rails

Note: we don’t use TDD at Codecademy.

Goals

We needed a few things from our testing setup:

  • The test suite should be fast, to encourage us to write more tests and save us time;
  • We should be able to test our React components effectively;
  • The test suite should play well with ES6 syntax and our webpack setup;
  • The test suite must be integrated alongside our current integration tests on CircleCI

If you want to read more about our frontend tech stack, my (Medium-averse, social-media-abstaining) coworker Artur wrote a great post about how we moved to ES6, Webpack, and React.

Attempt #1: Jest

Jest is mentioned in the official React documentation. “We use Jest,” the React team says. OK then. If the React team uses it, it’s probably worth trying!

Jest is pretty similar to Jasmine, but it introduces some additional features, like auto-mocking of all dependencies. It also makes it easy to fast-forward timers, so that you can check if callbacks have been fired:

jest.runAllTimers();

After getting Jest set up, I tried writing some tests. Here’s a shortened version of one such test:

jest.autoMockOff();
require('babel-core/polyfill');
var React = require('react/addons');
var RandomizedRow = require('../randomized_row');
var TestUtils = React.addons.TestUtils;

describe('randomized_row', function() {
it('can render a single item', function() {
// Render a row with 1 child
var row = TestUtils.renderIntoDocument(
<RandomizedRow><div id='testID'>test</div></RandomizedRow>
);
var renderedRow = TestUtils.findRenderedDOMComponentWithClass(
row, 'grid-row'
).getDOMNode();
expect(renderedRow.textContent).toBe('test');
expect(renderedRow.children.length).toBe(1);
});
});

Jest was slow. It took ~2 seconds to run each test file. That’s not really tenable.

Presumably this was due to Jest’s auto-mocking, but we didn’t particularly want auto-mocking, so this was not an acceptable trade-off for us.

Additionally, Jest doesn’t support ES6 syntax in tests, and it strongly favored relative-path import statements, which we don’t use. We usually write:

import MyComponent from ‘components/MyComponent’;

rather than:

import MyComponent from ‘../MyComponent’;

There’s probably a way around this, but I couldn’t figure it out, and it was annoying to deal with.

It also requires a polyfill in each file, to handle ES6 syntax in our own codebase, and requires you to explicitly disable automocking (which we usually didn’t want).

In general, it seemed that the Jest project wasn’t as actively maintained as it could be — seems like the main contributor has a lot on his plate. Between that and the slow speed of the tests, I wanted to explore an alternative.

Attempt #2: Karma + Jasmine

I decided to try Karma and Jasmine next, based on having heard good things about them. I found kimagure’s blog post on setting up React + Karma + Webpack particularly helpful:

With Karma, my main problem was figuring out how to match my karma.conf.js file with our webpack setup. This turned out to be pretty simple to address; I just required our webpack config and made use of it in the karma config.

var webpackConfig = require(__dirname + ‘/webpack.config’);

Everything was much easier after that:

module.exports = function(config) {
config.set({
basePath: ‘webpack/assets/javascripts/’,
frameworks: [‘jasmine’],
files: [
'mocks/globals.js',
'tests.webpack.js',
],
preprocessors: {
'tests.webpack.js': ['webpack', 'sourcemap']
},
webpack: {
resolve: webpackConfig.resolve,
module: webpackConfig.module
},
webpackServer: {
noInfo: true
},
reporters: ['progress'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: false,
browsers: ['PhantomJS'],
singleRun: true
});
});

The test looks quite similar to the Jest version.

import React from 'react/addons';
import RandomizedRow from 'components/randomized_row';
var TestUtils = React.addons.TestUtils;

describe('randomized_row', () => {
it('can render successfully', () => {
var row = TestUtils.renderIntoDocument(
<RandomizedRow>
<div id='testID'>test</div>
</RandomizedRow>
);
var renderedRow = TestUtils.scryRenderedComponentsWithType(
row, RandomizedRow
);
expect(renderedRow.length).toBe(1);
});

});

Note that there’s less boilerplate, and Jasmine + Karma is able to fully support ES6 because it plays nicely with our Webpack setup. This test took only 18 miliseconds to run locally, compared with almost 2 seconds with Jest. There is some setup time, since Karma launches a headless browser, but that’s per-run, not per-test.

To run tests, we added a simple entry to our package.json file:

"scripts": {
"test": "karma start"
}

And now we can run our tests with npm test. After that, integrating this into CircleCI was easy.

The result? Karma + Jasmine was over 100 times faster than Jest, and a better fit for our setup.

Surprises

We did encounter some surprising issues, like the fact that scryRenderedComponentsWithType appears to ignore child-trees of plain HTML elements. (Sigh.)

Conclusion: Karma + Jasmine + Webpack + React = ❤

We now use Karma + Jasmine to write tests for our React components, and we’re really happy with the combination.

I’m always curious to hear what other people are using, though — what works for you? What doesn’t? Let me know!