如何使用Mocha,Chai和&来测试方法和回调React-Redux中的酶

如何使用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.

  1. 您必须先使用酶的 shallow 来一次渲染组件.或 mount 函数,具体取决于您是否需要 [and/或您喜欢的方式] 模拟嵌套子项上的事件.这也为您提供了一个酶包装器,您可以从中访问组件实例及其方法.
  2. 您需要围绕这些实例方法包装 sinon测试间谍,然后使用 .update 以获得带有间谍的包装程序版本,您可以在其中进行间谍活动断言.
  1. You have to render the component once first, using enzyme's 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.
  2. 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);
    });
  });
});

注释:

  • 在将上述代码用于PlayerListPlayer时,我发现您没有将名为player的道具分配给Player;相反,您正在分配item={ item }.为了使它在本地工作,我将其更改为<Player player={ item } … />.
  • onSelect中,您要检查收到的i参数是否为false,然后调用selectPlayer(1).在上面的示例中,我没有为此提供测试用例,因为逻辑使我担心,原因有两个:
  • When using the above code for 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 } … />.
  • In 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:
  1. 我想知道i是否会成为0吗?如果是这样,它将始终评估为false并传递到该块中.
  2. 因为Player调用onSelect(this.props.player.id),所以我想知道this.props.player.id是否会是undefined吗?如果是这样,我想知道为什么您会在props.playerList中有一个没有id属性的项目.
  1. I wonder if i could ever be 0? If so, it will always evaluate as falsey and get passed into that block.
  2. Because 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…

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