The best way to unit test React components using ES6 with Babel

von Merle Ehm (Kommentare: 0)

Some days ago a colleague asked me how to write tests for React components. It wasn't just a challenge to write the tests but also to set up the whole environment to run the tests and measuring the code coverage. I did some googling to solve the problem and I realised that there are several ways to solve them. Some solutions work and some not. I will show you my solution that works!

Which testing framework to use?

It doens't make much difference if you use Jasmine or Mocha for writing your tests. I like Mocha because of its modular architecture. You can use any expectation or mocking framework with Mocha. Actually you will use Chai and Sinon. So I did.

npm install mocha --save-dev
npm install chai --save-dev
npm install sinon --save-dev

How to test a React component?

There seem to be two Frameworks to write unit tests for React components: ReactTestUtils and Enzyme. The API of Enzyme is more modern and easier to use. The decision to take Enzyme was easy.

npm install enzyme --save-dev

ModuleTolbar.jsx

import * as React from "react";

class ModuleToolbar extends React.Component {

 constructor(props) {
   super(props);
   this.state = { count: 0 };
 }

 render() {
   const { count } = this.state;
   return (
     <div>
       <div className={`clicks-${count}`}>
          {count} clicks
       </div>
       <a onClick={this.props.onButtonClick}>
          Increment
       </a>
     </div>
  );
 }
}

ModuleToolbarSpec.jsx

import { shallow } from 'enzyme';
import {describe, it, before, after} from 'mocha';
import {expect} from 'chai';
import sinon from 'sinon';
import ModuleToolbar from '../../';

describe('<ModuleToolbar />', () => {

  let wrapper;
  let onButtonClickSpy;

  before(() => {
     onButtonClickSpy = sinon.spy();
     wrapper = shallow(<ModuleToolbar onButtonClick={onButtonClickSpy}/>);     
  });

  after(() => {
     sinon.restore();
  });
  it('should render 2 <div> tags', () => {    
    expect(wrapper.find(<div>)).to.have.length(2);
  });

  it('should render 1 <a> tag', () => {
    expect(wrapper.find(<a>)).to.have.length(1);
  });

  it('should render a `.clicks-0`', () => {
    expect(wrapper.find('.clicks-0')).to.have.length(1);
  });

  it('should click button once', () => {   
    wrapper.find('button').simulate('click');
    expect(onButtonClickSpy.calledOnce).to.equal(true);    
  });

});

The shallow function just renders the current level of your component under test. It will not traverse down to the child components. This way it is possible to write a real unit test for your component.

How to run the unit tests with Babel?

The React components are written in ES6 and even the tests themselves are coded in ES6. So we need to transpile the code and the tests before running them with Mocha.

npm install babel-register --save-dev
npm install babel-preset-es2015 --save-dev
npm install babel-preset-react --save-dev

.babelrc

{ 
   "presets": [
       "babel-preset-es2015",
       "babel-preset-react"
   ]
}

package.json

"scripts": {
   "test": "mocha --compilers js:babel-register src/test/**/*Spec.jsx"
}

If you also use Webpack in your project there would be the possibility to run Babel via Webpack. I've seen many setups doing it this way. But why? It would unnecessarily slow down the build process because of doing an extra step.

How to mock the DOM?

Obviously the React framework needs a DOM to run. The simplest way would be to run the test in the browser using Karma. Your tests run this way as close to your production environment as possible because you can run your tests on different browsers like Chrome, Firefox or Edge. But do I need this precision for unit tests? In my opinion, not. The browser compatibility will be covered by integration tests or eventually by automated ui testing using tools like Selenium. To run the tests without Karma would even be a small performance boost for the test runtime. So I decided to run the tests in NodeJs using jsdom as a mock for the DOM. It works pretty well and it is very fast.

setup.js

import { JSDOM } from 'jsdom';

const jsdom = new JSDOM('<!doctype html><html><body></body></html>');

const { window } = jsdom;

function copyProps(src, target) {
    const props = Object.getOwnPropertyNames(src)
        .filter(prop => typeof target[prop] === 'undefined')
        .map(prop => Object.getOwnPropertyDescriptor(src, prop));
    Object.defineProperties(target, props);
}

global.window = window;
global.document = window.document;
global.navigator = {
    userAgent: 'node.js'
};
copyProps(window, global);

global.navigator = {
    userAgent: 'node.js'
};

package.json

"scripts": {
   "test": "mocha --require src/test/utils/setup.js --compilers js:babel-register src/test/**/*Spec.jsx"
}

How to get the correct code coverage for ES6?

My first attempt to get the code coverage in ES6 was a bad choice. The coverage seems to be calculated on the transpiled code. The import statements were marked as 2 branches where coverage on 1 branch is missing. But obviously an import statement takes only 1 branch to cover. The only way to get the correct code coverage was by using the babel-plugin-istanbul through nyc . This  is simply the command line interface of Istanbul.

npm install istanbul --save-dev
npm install nyc --save-dev
npm install babel-plugin-istanbul --save-dev

.babelrc

{ 
  "presets": [ ... ],
  "plugins": ["istanbul"]
}

package.json

"nyc": {
  "include": [
    "src/main/**/*.js*"
  ],
  "reporter": [
    "lcov",
    "text"
  ],
  "sourceMap": false,
  "instrument": false
},
"scripts": {
   "test": "nyc mocha --require src/test/utils/setup.js --compilers js:babel-register src/test/**/*Spec.jsx"
}

How to run a single test in WebStorm?

Running a single test is very helpful when you start coding them. You can even run the test in debug mode to solve your issues easily!

  1. Right click on the test file you want to run
  2. Select "Run <name of your file>" from sub menu
  3. Edit the configuration to add --require src/test/utils/setup.js --compilers js:babel-register into the field for the extra Mocha options

Zurück