如何使用Mocha,Chai和&来测试方法和回调React-Redux中的酶
我必须为PlayerList
容器和Player
组件编写单元测试用例.编写分支和道具的测试用例是可以的,但是如何测试组件的方法及其内部的逻辑.我的代码覆盖不完整,因为未对方法进行测试.
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.
场景:
父组件将对方法onSelect
的引用作为对子组件的回调.该方法在PlayerList
组件中定义,但是Player
正在生成调用它的onClick事件.
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.
父组件/容器:
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);
子组件:
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;
使用酶的.instance()
方法访问组件方法
当然有两个先决条件.
Use enzyme's .instance()
method to access component methods
There are a couple of prerequisites of course.
- 您必须先使用酶的
shallow
来一次渲染组件.或mount
函数,具体取决于您是否需要 [and/或您喜欢的方式] 模拟嵌套子项上的事件.这也为您提供了一个酶包装器,您可以从中访问组件实例及其方法. - 您需要围绕这些实例方法包装 sinon测试间谍,然后使用
.update
以获得带有间谍的包装程序版本,您可以在其中进行间谍活动断言.
- You have to render the component once first, using enzyme's
shallow
ormount
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. - You'll need to wrap sinon test spies around those instance methods and re-render using
.update
to get a version of the wrapper with spies on which you can assert.
示例:
// 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);
});
});
});
注释:
- 在将上述代码用于
PlayerList
和Player
时,我发现您没有将名为player
的道具分配给Player
;相反,您正在分配item={ item }
.为了使它在本地工作,我将其更改为<Player player={ item } … />
. - 在
onSelect
中,您要检查收到的i
参数是否为false,然后调用selectPlayer(1)
.在上面的示例中,我没有为此提供测试用例,因为逻辑使我担心,原因有两个:
- When using the above code for
PlayerList
andPlayer
, I discovered that you are not assigning a prop calledplayer
toPlayer
; instead you are assigningitem={ item }
. To get this working locally, I changed it to<Player player={ item } … />
. - In
onSelect
you are checking whether the receivedi
argument is falsey and then callingselectPlayer(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
是否会成为0
吗?如果是这样,它将始终评估为false并传递到该块中. - 因为
Player
调用onSelect(this.props.player.id)
,所以我想知道this.props.player.id
是否会是undefined
吗?如果是这样,我想知道为什么您会在props.playerList
中有一个没有id
属性的项目.
- I wonder if
i
could ever be0
? If so, it will always evaluate as falsey and get passed into that block. - Because
Player
callsonSelect(this.props.player.id)
, I wonder ifthis.props.player.id
would ever beundefined
? If so, I wonder why you would have an item inprops.playerList
with noid
property.
但是,如果您想像现在那样测试这种逻辑,它将看起来像这样……
But if you wanted to test that logic as it is now, it would look something like this…
onSelect
中的示例测试逻辑:
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);
});
});
});