Testing with ReactJS at Codecademy

Bonnie Eisenman
Jul 5, 2015 · 4 min read

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

  • 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 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

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

Conclusion: Karma + Jasmine + Webpack + React = ❤

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

About Codecademy

Lessons learned while teaching the world to code

About Codecademy

Lessons learned while teaching the world to code

Bonnie Eisenman

Written by

Software eng @ Twitter, author of Learning React Native. http://blog.bonnieeisenman.com/

About Codecademy

Lessons learned while teaching the world to code