【问题标题】:How to test methods and callback using Mocha, Chai, & Enzyme in React-Redux如何在 React-Redux 中使用 Mocha、Chai 和 Enzyme 测试方法和回调
【发布时间】:2017-01-16 16:57:09
【问题描述】:

我必须为PlayerList 容器和Player 组件编写单元测试用例。为分支和道具编写测试用例是可以的,但是我如何测试组件的方法和其中的逻辑。我的代码覆盖率不完整,因为方法没有经过测试。

场景:

父组件将对其方法onSelect 的引用作为回调传递给子组件。该方法在PlayerList 组件中定义,但Player 正在生成调用它的onClick 事件。

父组件/容器:

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;

【问题讨论】:

    标签: reactjs mocha.js chai enzyme


    【解决方案1】:

    使用酶的.instance() 方法访问组件方法

    当然有几个先决条件。

    1. 您必须先渲染一次组件,使用酶的 shallow 或 mount 函数,具体取决于您是否需要 [和/或您喜欢的方式] simulate 嵌套事件孩子们。这也为您提供了一个酶包装器,您可以从中访问组件实例及其方法。
    2. 您需要将sinon test spies 包裹在这些实例方法周围,并使用.update 重新渲染,以获得可以断言的带有间谍的包装器版本。

    示例:

    // 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 }。为了让它在本地工作,我将其更改为 &lt;Player player={ item } … /&gt;。
    • 在onSelect 中,您正在检查接收到的i 参数是否为假,然后调用selectPlayer(1)。在上面的示例中,我没有为此包含测试用例,因为逻辑与我有关,原因有两个:
      1. 我想知道i 是否可以成为0?如果是这样,它将始终评估为错误并传递到该块中。
      2. 因为Player 调用onSelect(this.props.player.id),我想知道this.props.player.id 是否会成为undefined?如果是这样,我想知道为什么您会在 props.playerList 中有一个没有 id 属性的项目。

    但是如果你想测试现在的逻辑,它看起来像这样......

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

    【讨论】:

    • 你拯救了我的一天。赞赏!
    【解决方案2】:

    您可以使用酶的simulate 函数来测试回调。您可以提供回调函数作为 sinon 的 spy 函数,并使用预期的参数验证它是否已被调用预期次数。 您可以在此处阅读更多信息: https://github.com/airbnb/enzyme/blob/master/docs/api/ShallowWrapper/simulate.md

    这里是模拟回调函数的 Player 和 PlayerList 组件的单元测试。您需要单独的 PlayerList 组件作为 PlayerList 和 PlayerListContainer(表示该组件连接到 redux)。完成此操作后,您可以轻松地测试您的 PlayerList 组件。

    PlayerListTest.jsx:

    import React from 'react';
    import { shallow } from 'enzyme';
    import { expect } from 'chai';
    import sinon from 'sinon';
    import PlayerList from 'components/PlayerList';
    import Player from 'components/Player';
    
    describe('PlayerList test', () => {
      const playerList = [
        {
          id: '1',
          imgUrl: 'testimageurl',
          name: 'testplayer1'
        },
        {
          id: '23423',
          imgUrl: 'http://testimageurl2',
          name: 'testplayer2'
        },
        {
          id: '123124123',
          imgUrl: 'http://testimageurl23',
          name: 'testplayer142'
        }
      ];
    
      it('calls callback function with item id when player is selected', () => {
        const mockSelectPlayer = sinon.spy();
        const wrapper = shallow(<PlayerList playerList={playerList} selectPlayer={mockSelectPlayer} />);
    
        const playerWrapper = wrapper.find(Player);
        playerWrapper.at(0).simulate('select');
    
        expect(mockSelectPlayer.calledOnce).to.equal(true);
        expect(mockSelectPlayer.calledWith(playerList[0].id)).to.be.ok;
      });
    
    
    });
    

    PlayerTest.jsx:

    import React from 'react';
    import { shallow } from 'enzyme';
    import { expect } from 'chai';
    import sinon from 'sinon';
    import Player from 'components/Player.jsx';
    
    describe('Player test', () => {
      const player = {
        id: '1234',
        imgUrl: 'http://testimageurl',
        name: 'testplayer'
      };
    
      it('calls callback function when the .player-container element clicked', () => {
        const mockOnSelect = sinon.spy();
        const wrapper = shallow(<Player player={player} onSelect={mockOnSelect} />);
    
        wrapper.find('.player-container').simulate('click');
    
        expect(mockOnSelect.calledOnce).to.equal(true);
      });
    });
    

    【讨论】:

    • 这没有显示 OP 描述的场景,其中嵌套组件触发类方法。
    • 您可以使用酶的mount 方法来完整渲染您的组件。这样就可以使用酶的simulate调用嵌套组件的回调函数。
    • 您能否包含一个示例单元测试,展示如何通过模拟点击监视和调用父组件的方法?
    • 这是作为 props 传递的单元测试回调函数的一个很好的例子,但你没有展示“如何测试 [the] 方法和方法内部的逻辑”,其中“[the] 方法在父组件”。 OP 想测试PlayerList.onSelect。
    • 您可以通过将undefined 再次分配给 playerId 和 simulate select 事件来测试它。在这种情况下,应该使用1 调用回调函数。您不必直接调用组件上的每个方法来测试您的组件。
    猜你喜欢
    • 2017-06-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-20
    • 2018-03-22
    • 2016-06-13
    • 2017-12-16
    相关资源
    最近更新 更多