Testing Redux next and api.dispatch with TypeScript and Jest
Posted on: 2018-11-07
A middleware in Redux can have quite a lot of logic. In fact, this is my favorite place to place logic. The rationale is that it can be triggered by an action, still, have the time to request data from the backend server with an Ajax call and can dispatch other actions that can be computed by another middleware or by a reducer. It becomes crucial to unit test any area where logic can go wrong, thus testing if specific logic dispatch or invoke next.
Testing api.dispatch is a matter of leveraging Jest. If you are using React and Redux, it is the defacto testing framework. In fact, it is the one coming with the famous "create-react-app". To test if an action got dispatched from the api.dispatch, you need to mock the dispatch and verify that it has been called. The function toHavebeenCalledWith
can take another expect which can peek the content of an object passed. Because every action contains a type and payload, it is possible to verify if an action has been invoked.
middleware.myFunctionInMiddleware(next, api, payload);
expect(api.dispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: MY_ACTION_CONSTANT_HERE
})
);
The case of next is similar. I found that while it is possible to use a similar logic than with api.dispatch, that sometimes it is not enough. For example, if you have a function that calls several times next
. In that case, it is possible to pass a custom next
that will be smarter than a simple mock.
let nextMock = nextPayloadTypeSpy(MY_ACTION_CONSTANT_HERE);
middleware.myFunctionInMiddleware(nextMock.mock, api, payload);
expect(nextMock.getPayload()[0]).toBe(payload);
expect(nextMock.hasBeenCalled()).toBeTruthy();
The code above this paragraph is a glimpse of how to use the smart next
. The code accumulates in an array all invocation and allows to asset its content along the test. In that case, the test was testing the first execution of the next
associated to a specific action (defined at the declaration of the object). The logic relies on a custom Jest's function that adds in an array all actions of a specific time when invoked by the tested function.
export interface ActionsWithPayload<TypeAction, TypePayload> {
type: TypeAction;
payload: TypePayload;
}
export interface SpyActionsWithPayload {
mock: jest.Mock<{}>;
hasBeenCalled: () => boolean;
getPayload: () => any[];
}
export function nextPayloadTypeSpy(type?: string): SpyActionsWithPayload {
let typeFromCaller: string[] = [];
let payloadFromCaller: any[] = [];
let nextMock = jest.fn().mockImplementation((func: ActionsWithPayload<any, string>) => {
typeFromCaller.push(func.type);
if (func.type === type) {
payloadFromCaller.push(func.payload);
}
});
return {
mock: nextMock,
hasBeenCalled: () => {
return type === undefined ? false : typeFromCaller.includes(type);
},
getPayload: () => payloadFromCaller
};
}
The code is tightly coupled with how you are handling your action. I strongly suggest using the Redux-TypeScript boilerplate "free". It is really a relief to use in a term that you will create action within 30 seconds and to be type safe for the payload.
The code uses a "mock" property which is a hook to a mock implementation that does nothing else than being recorded in an array. The actual action is doing nothing. The two functions aside the mock property are there to assert the test. Future improvements are obvious. For example, the hasBeenCalled
could also take an index to ensure that a particular "next" has been called.
To summarize, even if the middleware design is frightful, with types and some useful utility functions and patterns creating code and testing this one afterward is a breeze. I always enjoy having tests that are quick to build, and the discussed approach rationale with that mindset.