Rules for testing React UI component with Enzyme

You should always write test for your code.

All the examples below will be using Mocha, Chai, you should be able to change to whatever library you like.

Common rules for test

First of all, you should give your test suit a good name, I prefer prefix you suite name with your module type and followed by the moudue name, for example:

describe('[Component] DatePicker', () => {}) // Use PascalCase with your module type.

Secondly, give a specific description for each test case, use word “should” at the beginning to describe the expected behavior, like:

it('should show result view in response to currentView prop', () => {})

Test for component

Use enzyme to shallow render components in test cases. You should read about its API: shallow, mount and Enzyme Selectors

import { shallow } from 'enzyme';
import { expect } from 'chai';
import List from './comp.List.js';
describe('[component] List', () => {
  it('should render list items correctly', () => {
    const wrapper = shallow(<List />);
    expect(wrapper.find({ 'data-ref': 'listItem' }).length).to.equal(3);
  });
});

Notice the example uses { 'data-ref': 'listItem' } as the Enzyme Selector. data-ref is a convention attribute name I use to refer certain element for test:

// comp.List.js
import ListItem from './comp.ListItem';
function renderListItem(list) {
  return list.map(item => {
    return <ListItem data-ref="listItem">{item.name}</ListItem>;
  });
}

You should avoid using Component class as the enzyme selector, which couples your test cases to a certain implementation, bellow is a bad example:

import { shallow } from 'enzyme';
import { expect } from 'chai';
import List from './comp.List.js';
import ListItem from './comp.ListItem';
describe('[component] List', () => {
  it('should render list items correctly', () => {
    const wrapper = shallow(<List />);
    // BAD:
    // whenever your implementation for the List change
    // you may have to rewrite your test case
    expect(wrapper.find(ListItem).length).to.equal(3);
  });
});

Tricks for shallow and mount

The difference between shallow() and mount() is that mount() renders your component into real dom tree, shallow() only create a shallow render tree.

For shallow rendered elements:

You can use simulate() to simulate event for shallow rendered element, but you need to provide event object by yourself:

el.simulate('click', {});

The event does not propagate, you do not expect simulating a click event on one element will trigger some event else, in most of the cases you should simulate event you need directly:

popover.simulate('open');

Another thing you need to notice is that you are not able to select a nested component directly with shallow rendered element.

For example, you created a component Toolbar

// toolbar
export default function Toolbar(props) {
  return <div>
    {props.tools.map(tool => {
      return <div data-ref="toolItem">{tool}</div>
    })}
  </div>;
}

Then you created another component using Toolbar:

// listWithToolbar.js
import List from './list.js';
import Toolbar from './toolbar.js';
export default function ListWithToolbar(props) {
  return <div>
    <Toolbar tools={props.tools} data-ref="toolbar" />
    <List list={props.list} />
  </div>;
}

Now in your test you will not be able to get all the “toolItem” directly:

it('should render tool items correctly', () => {
  const tools = [..];
  const list = [...]
  const wrapper = shallow(<ListWithToolbar tools={tools} list={list} />);
  // this will fail, since the result will be 0
  expect(wrapper.find({ 'data-ref': 'toolItem' }).length).to.equal(tools.length);
});

The reason it will fail is that shallow() only render one level of the component, like:

<div>
  <Toolbar tools={props.tools} />
  <List list={props.list} />
</div>

If you need to go deep further, you need to get Toolbar first and call shallow() to shallow render it:

it('should render tool items correctly', () => {
  const tools = [..];
  const list = [...];
  const wrapper = shallow(<ListWithToolbar tools={tools} list={list} />);
  const toolbar = wrapper.find({ 'data-ref': 'toolbar' }).shallow();
  expect(toolbar.find({ 'data-ref': 'toolItem' }).length).to.equal(tools.length);
});

For Mount rendered elements:

You can use simulate() to simulate event for Mount rendered element, it will be just a normal DOM event, and you can only simulate a normal DOM event, like:

el.simulate('click');

The difference here between shallow() is you don’t need to provide event object by yourself and the event can propagate.

Since you are dealing with a whole complete dom tree, you can search for nested element directly:

it('should render tool items correctly', () => {
  const tools = [..];
  const list = [...]
  const wrapper = mount(<ListWithToolbar tools={tools} list={list} />);
  // this will pass
  expect(wrapper.find({ 'data-ref': 'toolItem' }).length).to.equal(tools.length);
});

But when you try to get your target’s properties or use property selector it may fail because the final rendered dom tree may not contain the property it has in a shallow tree, for example:

You have a simple Tip component:

export default function Tip(props) {
  return <span>{props.tip}</span>;
}

Then you created another one for ResultSuccess:

export default function ResultSuccess(props) {
  return <div>
  SUCCESS
  <Tip data-ref="tip" tip="Congratulations´╝üYou just passed the test." />
  </div>;
}

You will fail to get the tip element by using the selector { 'data-ref': 'tip' }, since, for the final dom three, it is like this:

<div>
  SUCCESS
  <span>Congratulations´╝üYou just passed the test.</span>
</div>

Test for callbacks

Use Sinon for callback tests.

Test if the callback is called:

import Sinon from 'sinon';
import { expect } from 'chai';
const onChange = Sinon.spy();
onChange();
expect(onChange.called).to.equal(true);
expect(onChange.calledOnce).to.equal(true);

Test if the callback is called with certain args:

import Sinon from 'sinon';
import { expect } from 'chai';
const callbacks = { onChange: () => {} );
const onChange = Sinon.spy(callbacks, 'onChange'};
onChange(1, 2);
expect(onChange.calledWith(1,2)).to.equal(true);

Leave a Comment

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.