Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to unit test mapDispatchToProps with thunk action

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 component:

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-component";
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?

like image 276
CodingIntrigue Avatar asked Jan 13 '17 11:01

CodingIntrigue


2 Answers

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
like image 124
tony19 Avatar answered Sep 18 '22 06:09

tony19


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.

like image 33
DarkKnight Avatar answered Sep 19 '22 06:09

DarkKnight