How to test methods and callback using Mocha, Chai, & Enzyme in React-Redux

不羁岁月 提交于 2020-01-01 12:27:40

问题


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;

回答1:


Use enzyme's .instance() method to access component methods

There are a couple of prerequisites of course.

  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.

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:

  • 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 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…

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



回答2:


You can use enzyme's simulate function to test callbacks. You can provide callback functions as spy functions from sinon and verify that it has been called expected times with expected arguments. You can read more on this here: https://github.com/airbnb/enzyme/blob/master/docs/api/ShallowWrapper/simulate.md

Here is the unit tests for Player and PlayerList components simulating the callback functions. You need the separate PlayerList component as PlayerList and PlayerListContainer(indicating that the component connected to redux). After doing this, you can easily test your PlayerList component.

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


来源:https://stackoverflow.com/questions/39396357/how-to-test-methods-and-callback-using-mocha-chai-enzyme-in-react-redux

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!