最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to unit test mapDispatchToProps with thunk action - Stack Overflow

programmeradmin6浏览0评论

I have the following Redux action creator:

export const keyDown = key => (dispatch, getState) => {
    const { modifier } = getState().data;
    dispatch({ type: KEYDOWN, key });
    return handle(modifier, key); // Returns true or false
};

And the following connected ponent:

export const mapDispatchToProps = dispatch => ({
    onKeyDown: e => {
        if(e.target.tagName === "INPUT") return;
        const handledKey = dispatch(keyDown(e.keyCode));
        if(handledKey) {
            e.preventDefault();
        }
    }
});

I am trying to write a test to ensure that dispatch is called with the keyDown action when the tagName is anything other than "INPUT". This is my test:

import { spy } from "sinon";
import keycode from "keycodes";
import { mapDispatchToProps } from "./connected-ponent";
import { keyDown } from "./actions";

// Creates a simple Event stub...
const createEvent = (tag, keyCode) => ({
    target: {
        tagName: tag.toUpperCase()
    },
    preventDefault: spy(),
    keyCode
});

it("Dispatches a keyDown event with the specified keyCode if the selected element is not an <input>", () => {
    const dispatch = spy();
    const keyCode = keycode("u");
    mapDispatchToProps(dispatch).onKeyDown(createEvent("div", keyCode));
    // This fails...
    expect(dispatch).to.have.been.calledWith(keyDown(keycode));
});

Presumably this is something to do with using arrow functions? Is there any way I can ensure that dispatch was called with the function signature that I expect?

I have the following Redux action creator:

export const keyDown = key => (dispatch, getState) => {
    const { modifier } = getState().data;
    dispatch({ type: KEYDOWN, key });
    return handle(modifier, key); // Returns true or false
};

And the following connected ponent:

export const mapDispatchToProps = dispatch => ({
    onKeyDown: e => {
        if(e.target.tagName === "INPUT") return;
        const handledKey = dispatch(keyDown(e.keyCode));
        if(handledKey) {
            e.preventDefault();
        }
    }
});

I am trying to write a test to ensure that dispatch is called with the keyDown action when the tagName is anything other than "INPUT". This is my test:

import { spy } from "sinon";
import keycode from "keycodes";
import { mapDispatchToProps } from "./connected-ponent";
import { keyDown } from "./actions";

// Creates a simple Event stub...
const createEvent = (tag, keyCode) => ({
    target: {
        tagName: tag.toUpperCase()
    },
    preventDefault: spy(),
    keyCode
});

it("Dispatches a keyDown event with the specified keyCode if the selected element is not an <input>", () => {
    const dispatch = spy();
    const keyCode = keycode("u");
    mapDispatchToProps(dispatch).onKeyDown(createEvent("div", keyCode));
    // This fails...
    expect(dispatch).to.have.been.calledWith(keyDown(keycode));
});

Presumably this is something to do with using arrow functions? Is there any way I can ensure that dispatch was called with the function signature that I expect?

Share Improve this question asked Jan 13, 2017 at 11:12 CodingIntrigueCodingIntrigue 78.7k32 gold badges176 silver badges177 bronze badges 2
  • So are you testing that string equality and the return statement works, or that a developer didn't accidentally remove it? God I dislike most unit tests :( – nanobar Commented Jan 16, 2017 at 14:18
  • Primarily that the dispatch is actually called. A lot of times I call the action creator without passing to dispatch. Checking that the keyDown action is passed important too, so expect(dispatch).to.have.been.called wouldn't be enough I don't think – CodingIntrigue Commented Jan 16, 2017 at 14:20
Add a ment  | 

3 Answers 3

Reset to default 3 +500

Simplest solution might be to memoize keyDown() as suggested in another answer (+1). Here's a different approach that attempts to cover all bases...


Since keyDown() is imported from actions, we could stub the function to return a dummy value whenever it gets called with keyCode:

import * as actions;
keyDown = stub(actions, "keyDown");
keyDown.withArgs(keyCode).returns(dummy);

Then, our unit tests would verify dispatch was called with the dummy that we had previously setup. We know the dummy can only be returned by our stubbed keyDown(), so this check also verifies that keyDown() was called.

mapDispatchToProps(dispatch).onKeyDown(createEvent("div", keyCode));
expect(dispatch).to.have.been.calledWith(dummy);
expect(keyDown).to.have.been.calledWithExactly(keyCode);

To be thorough, we should add unit tests to confirm that the key event is not dispatched when the target is an <input>.

mapDispatchToProps(dispatch).onKeyDown(createEvent("input", keyCode));
expect(dispatch).to.not.have.been.called;
expect(keyDown).to.not.have.been.called;

We should also test keyDown() itself in isolation by verifying that the given dispatch callback is invoked with the correct key event and key code:

expect(dispatch).to.have.been.calledWith({type: actions.KEYDOWN, key: keyCode});

Links

  • GitHub demo
  • 100% test coverage

As @DarkKnight said (got +1), keyDown is returning a new function for every invocation, so the test fails because keyDown(keyCode) != keyDown(keyCode).

If you don't want to change your actual implementation of keyDown, you can just mock in your tests:

import * as actions from "./actions";   

spyOn(actions, 'keyDown');  

You can see other answers on how it can be done:

  • How to mock the imports of an ES6 module?

  • How to mock dependencies for unit tests with ES6 Modules

keyDown(keycode) creates a new function every time, and every function instances are different, the test case fails as expected.

This can be fixed by memorize functions created by keyDown:

let cacheKeyDown = {};
export const keyDown = key => cacheKeyDown[key] || cacheKeyDown[key] = (dispatch, getState) => {
    const { modifier } = getState().data;
    dispatch({ type: KEYDOWN, key });
    return handle(modifier, key);
};

With memorization, keyDown calls with same keycode return the same function.

发布评论

评论列表(0)

  1. 暂无评论