Testing react components with mocha.

moving away from jasmine and karma

Like most people I started my javascript unit testing experience with jasmine, adding karma as the test running for AngularJS and React projects.

Everything was gravy until es2015 came along (back in those days we called it es6).

Fast forward 6 months and every one is unit testing with jasmine and karma, transpiled by babel and loaded with webpack.

At the point where you test config, webpack config, babel config and karma config are more lines than your code things get annoying.

Then I discovered mocha, or to be more accurate, discovered that you can pass the babel-register as a mocha param. That meant no karma, no webpack and no config files but still allowed for tests and source code written in es2015.

The set up

Heres the dependencies you should install for a solid test set up:

  • npm i babel-core babel-preset-es2015 babel-preset-react babel-preset-stage-2
  • npm i chai mocha enzyme sinon
  • npm i react react-addons-test-utils react-dom

n.b. react-addons-test-utils and react-dom are implicit dependencies of react.

Then you’ll need to pass babel-core/register to mocha, so in your test script:

"test": "mocha -w --compilers js:babel-core/register '[glob to match test files]'"
// a glob might be something like: **/*.spec.js
// this would match any file whose names ends with .spec.js in any directory

You’ll need to declare your babel plugins, which you can do in your package.json or a .babelrc.

// .babelrc
{
  "presets": ["es2015", "react", "stage-2"],
  "ignore": "node_modules"
}

// package.json
"babel": {
  {
    "presets": ["es2015", "react", "stage-2"],
    "ignore": "node_modules"
  }
}

some components to test

Lets add two simple components. One is a save button and the other rendered a list of names passed through props as an array. By no means a perfect component, the myComponent component will demonstrate how we can test state, rendering and click events/even handlers.

saveButton.js

import React from 'react';

export default (props) => {
  return (<button>save</button>);
}

myComponent.js

import React from 'react';
import Save from './saveButton';

export default React.createClass({
  getInitialState() {
    return {
      title: 'list'
    }
  },
  selected(evt) {
    this.setState({
      title: `you selected ${evt.target.innerText}`
    })
  },
  renderListItem(name) {
    return (
      <li onClick={this.selected}>{name}</li>
    );
  },
  renderList() {
    if (this.props.names) {
      return (
        <ul>
          { this.props.names.map(this.renderListItem) }
        </ul>
      );
    }
    return null;
  },
  render() {
    return (
      <div>
        <h3>{this.state.title}</h3>
        { this.renderList() }
        <Save onClick={this.props.onSave} />
      </div>
    );
  }
});

myComponent.spec.js

Heres out test file

import React from 'react';
import { shallow, mount } from 'enzyme';
import { expect } from 'chai';
import sinon from 'sinon';

import MyComponent from './myComponent';
import SaveButton from './saveButton';


describe('<MyComponent />', () => {
  describe('when rendered', () => {
   let component, sandbox, eventHandler;

    beforeEach(() => {
      sandbox = sinon.sandbox.create();
      eventHandler = sandbox.stub();
      component = shallow(<MyComponent onSave={eventHandler} />);
    });

    it('should have a <div> as it\'s root elment', () => {
      expect(component.is('div')).to.equal(true);
    });

    it('should have a save button', () => {
      expect(component.containsMatchingElement(SomeButton)).to.equal(true);
    });

    describe('when the save button is clicked', () => {
      it('should call any callback functionality', () => {
        sinon.assert.notCalled(eventHandler);
        component.find(SomeButton).simulate('click');
        sinon.assert.calledOnce(eventHandler);
      });
    });
});

describe('when given a list of names', () => {
    let component, items, title;
    let names = ['Andrew', 'Bob', 'Steve', 'Colin'];

    beforeEach(() => {
      component = shallow(<MyComponent names={names} />);
      items = component.find('li');
      title = component.find('h3');
    });

    it('should show a unordered list containing those names', () => {
      expect(items).to.have.length(names.length);

      items.forEach((item, index) => {
        expect(item.text()).to.equal(names[index]);
      });
    });

    describe('the lists title', () => {
      it('should initially say \'list\'', () => {
        expect(title.text()).to.equal('list');
      });

      describe('when a list item has been clicked', () => {
        let item;

        beforeEach(() => {
          item = component.find('li').first();
          item.simulate('click', {target: {innerText: item.text()}});
          title = component.find('h3');
        });

        it('should update the title with the item clicked', () => {
          expect(title.text()).to.equal('you selected Andrew');
        });
      });
    });
  });
});

To test our code we run the test script:

npm test

If everything is working correctly you’ll see 6 passing tests and a warning about adding keys to iterated output (i.e. when we map over a function that returns a component we should also add a unique key).

benefits

We’ve just tested a es2015 syntax component, with a es2015 syntax test file and we haven’t needed karma or webpack and avoided the need for several lengthy config files.

Using mocha and passing babel-core/register gives much, much faster test times. On large codebases using this approach can really save you lots of time waiting for feedback from your test runner.

You’ll notice that we pass -w to mocha, which mean it will re-run on file changes giving you super fast feedback