Testing React Components with Mocha and JSDOM

You can see a working example of the whole setup in my example repository on GitHub.

I have been working on a large scale React project at work for the past year or so. We have hundreds of components, a few stores in our fluxy architecture, and a whole lot of testing to do. Originally, we had a setup where we used Mocha/Karma running in a PhantomJS instance. This worked pretty well, and meant that our unit tests were running directly in a browser. The problem was that it took forever to load up Karma/Phantom, and pretty long to run the 600+ tests we had written.

When testing a react component, you generally scaffold it up and render it using React's test util function renderIntoDocument(). This will render the component with a given set of props and return it. From there, you can poke and prod at the component, assert states and refs, and even look at the rendered HTML. You'd think you'd need a browser to do that, but here comes JSDOM with some wicked tricks up its sleeves.

JSDOM allows you to have a DOM-like environment entirely in a Node.js backend. Put simply, it fakes a DOM, so you can trick React into thinking it is rendering a component into a real DOM. With that, you can set up a test suite that uses the same technologies that you're used to, and have your tests written (damn near) exactly the way you would write them for a browser, but it all runs in the backend without the need of PhantomJS.

Speed

Our original testing framework took ~84 seconds to launch PhantomJS, compile JSX, bundle (with browserify) and begin running. A test suite of around 600 tests ran in about 40 seconds. With the JSDOM implementation, it dropped the startup time to 8 seconds, with tests running as fast as 20 seconds.

These numbers can vary based on how much JSX compiling is happening, and if you are using babel with caching to compile the JSX. You may see a slight additional setup time if you are using some form of instrumentation for code coverage such as Istanbul (which works great with this method, by the way!)

Walkthrough

Its surprisingly simple, actually. The first thing you need to do is install jsdom as a dependency for your repository. Next, create a scaffolding file that will be required in to your test suite:

// setup.js
'use strict';

import jsdom from 'jsdom';

// Define some html to be our basic document
// JSDOM will consume this and act as if we were in a browser
const DEFAULT_HTML = '<html><body></body></html>';

// Define some variables to make it look like we're a browser
// First, use JSDOM's fake DOM as the document
global.document = jsdom.jsdom(DEFAULT_HTML);

// Set up a mock window
global.window = document.defaultView;

// Allow for things like window.location
global.navigator = window.navigator;  

You can require this file in your npm test script using Mocha's --require method:

mocha --require setup.js  

If you want to compile JSX files on the fly for your tests, you can do that, too! You just need to install babel-require and set up your .babelrc file with the react plugin. If you don't already have babel-preset-react as dependencies for your project, you'll need that too.

npm install babel-require babel-preset-react  

And the .babelrc file:

{
  "presets": ["react"]
}

Now, you just specify babel-require as the compiler for JS files in you test script:

mocha --require setup.js --compilers js:babel-require  

Now you have mocha compiling your JSX files, setting up the scaffolding for JSDOM, and running your tests entirely in Nodeland!

If your project is anything like mine, you will see massive speed boosts in your test setup and runtime.

Notes

While JSDOM is fairly comprehensive, there are some places where you may need to make a few tweaks, either to your implementation, or to your test scaffolding. The biggest one I ran into was the lack of a globally defined Image class, so code that tried to dynamically generate an Image element failed. This was easily fixed by adding this line to my test config:

global.Image = window.Image;  

You may also run into issues where other functions (perhaps from dependencies) are accessing window-scoped functions or variables without using the window. prefix. This can be fixed pretty simply with the same pattern:

global.myErroringReference = window.myErroringReference  

This will cover most issues like this, but there may be some totally unimplemented features. One example is Document.getElementFromPoint, which is experimental to begin with, but beyond the capabilities of JSDOM. For situations like that, the best you can do is mock out the functions you need using a package like sinon.

If you have any issues with this method or see an error in the example, please open an issue or pull request to the example repository.