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);