I have to write unit test cases for a PlayerList
container and Player
component. Writing test cases for branches and props is OK, but how do I test the component's methods and the logic inside them. My code coverage is incomplete because the methods are not tested.
Scenario:
Parent component passes a reference to its method onSelect
as a callback to child component. The method is defined in PlayerList
component, but Player
is generating the onClick event that calls it.
Parent Component/Container:
import React, { Component } from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {selectTab} from '../actions/index';
import Player from './Player';
class PlayerList extends Component {
constructor(props){
super(props);
}
onSelect(i) {
if (!i) {
this.props.selectPlayer(1);
}
else {
this.props.selectPlayer(i);
}
}
createListItems(){
return this.props.playerList.map((item, i)=>{
return (
<Player key={i} tab={item} onSelect={() => this.onSelect(item.id)} />
)
});
}
render() {
return(
<div className="col-md-12">
<ul className="nav nav-tabs">
{this.createListItems()}
</ul>
</div>
)
}
}
function mapStateToProps(state){
return {
playerList: state.playerList
}
}
function matchDispatchToProps(dispatch){
return bindActionCreators({selectPlayer: selectPlayer}, dispatch);
}
export default connect(mapStateToProps, matchDispatchToProps)(PlayerList);
Child Component:
import React, { Component } from 'react';
class Player extends Component {
constructor(props){
super(props);
}
render() {
return(
<li className={this.props.player.selected?'active':''}>
<a href="#" onClick={() => this.props.onSelect(this.props.player.id)}>
<img src={this.props.player.imgUrl} className="thumbnail"/>
{this.props.player.name}
</a>
</li>
)
}
}
export default Player;
Testing async code with Mocha using callbacks is pretty straight forward, all you need to do is pass the done function down the callback chain and ensure it is executed after your last assertion: Let's try this same test but using promises instead.
Installing Mocha and Chai Launch this project in any of the source-code editors (Using VS Code here). Step 4: Create two folders named src and test respectively. While src stores the main file where the source code of the program is written, the test folder stores test cases for unit testing. Step 5: Create an app.
Running this command instructs Mocha to attach a special run() callback function to the global context. Calling the run() function instructs it to run all the test suites that have been described. Therefore, run() can be called after the asynchronous operation is completed to run the tests.
.instance()
method to access component methodsThere are a couple of prerequisites of course.
shallow
or mount
functions depending on whether you need to [and/or how you prefer to] simulate events on nested children. This also gives you an enzyme wrapper from which you'll access the component instance and its methods..update
to get a version of the wrapper with spies on which you can assert.Example:
// Import requisite modules
import React from 'react';
import sinon from 'sinon';
import { mount } from 'enzyme';
import { expect } from 'chai';
import PlayerList from './PlayerList';
// Describe what you'll be testing
describe('PlayerList component', () => {
// Mock player list
const playerList = [
{
id : 1,
imgUrl: 'http://placehold.it/100?text=P1',
name : 'Player One'
}
];
// Nested describe just for our instance methods
describe('Instance methods', () => {
// Stub the selectPlayer method.
const selectPlayer = sinon.stub();
// Full DOM render including nested Player component
const wrapper = mount(
<PlayerList playerList={ playerList } selectPlayer={ selectPlayer } />
);
// Get the component instance
const instance = wrapper.instance();
// Wrap the instance methods with spies
instance.createListItems = sinon.spy(instance.createListItems);
instance.onSelect = sinon.spy(instance.onSelect);
// Re-render component. Now with spies!
wrapper.update();
it('should call createListItems on render', () => {
expect(instance.createListItems).to.have.been.calledOnce;
});
it('should call onSelect on child link click', () => {
expect(instance.onSelect).to.not.have.been.called;
wrapper.find('li > a').at(0).simulate('click');
expect(instance.onSelect).to.have.been.calledOnce;
expect(instance.onSelect).to.have.been.calledWith(playerList[0].id);
});
});
});
Notes:
PlayerList
and Player
, I discovered that you are not assigning a prop called player
to Player
; instead you are assigning item={ item }
. To get this working locally, I changed it to <Player player={ item } … />
.onSelect
you are checking whether the received i
argument is falsey and then calling selectPlayer(1)
. I didn't include a test case for this in the above example because the logic concerns me for a couple of reasons:
i
could ever be 0
? If so, it will always evaluate as falsey and get passed into that block.Player
calls onSelect(this.props.player.id)
, I wonder if this.props.player.id
would ever be undefined
? If so, I wonder why you would have an item in props.playerList
with no id
property.But if you wanted to test that logic as it is now, it would look something like this…
Example testing logic in onSelect
:
describe('PlayerList component', () => {
…
// Mock player list should contain an item with `id: 0`
// …and another item with no `id` property.
const playerList = [
…, // index 0 (Player 1)
{ // index 1
id : 0,
imgUrl: 'http://placehold.it/100?text=P0',
name : 'Player Zero'
},
{ // index 2
imgUrl: 'http://placehold.it/100?text=P',
name : 'Player ?'
}
];
describe('Instance methods', { … });
describe('selectPlayer', () => {
const selectPlayer = sinon.stub();
const wrapper = mount(
<PlayerList playerList={ playerList } selectPlayer={ selectPlayer } />
);
const instance = wrapper.instance();
// There is no need to simulate clicks or wrap spies on instance methods
// …to test the call to selectPlayer. Just call the method directly.
it('should call props.selectPlayer with `id` if `id` is truthy', () => {
instance.onSelect(playerList[0].id); // id: 1
expect(selectPlayer).to.have.been.calledOnce;
expect(selectPlayer).to.have.been.calledWith(playerList[0].id);
});
it('should call props.selectPlayer(1) if `id` is 0', () => {
instance.onSelect(playerList[1].id); // id: 0
expect(selectPlayer).to.have.been.calledTwice;
expect(selectPlayer).to.have.been.calledWith(1);
});
it('should call props.selectPlayer(1) if `id` is undefined', () => {
instance.onSelect(playerList[2].id); // undefined
expect(selectPlayer).to.have.been.calledThrice;
expect(selectPlayer).to.have.been.calledWith(1);
});
});
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With