9

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?

CodingIntrigue
  • 75,930
  • 30
  • 170
  • 176
  • 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 :( – Dominic Jan 16 '17 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 Jan 16 '17 at 14:20

3 Answers3

3

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:

Community
  • 1
  • 1
Yoav Aharoni
  • 2,672
  • 13
  • 18
3

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

tony19
  • 125,647
  • 18
  • 229
  • 307
2

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.

DarkKnight
  • 5,651
  • 2
  • 24
  • 36