4
votes

I have a middleware which intercept any actions with a type that include: API_REQUEST. An action with API_REQUEST is created by the apiRequest() action creator. When my middleware intercept an action it makes a request using Axios, if the request succeeded it dispatch the action created by apiSuccess(). If Axios throw after the request, the middleware is going to dispatch the action created with apiError().

Middleware:

const apiMiddleware: Middleware = ({ dispatch }) => next => async (action): Promise<void> => {
    next(action);

    if (action.type.includes(API_REQUEST)) {
        const body = action.payload;
        const { url, method, feature } = action.meta;

        try {
            const response = await axios({ method, url, data: body });
            dispatch(apiSuccess({ response, feature }));
        } catch (error) {
            console.error(error);
            dispatch(apiError({ error, feature }));
        }
    }
};

This is how my api middleware works.

Now I'm wondering how can I test that using Jest. Maybe I can mock Axios so it makes a fake request in the middleware, but how?

Here's the current test file I have:

describe('api middleware', () => {
    const feature = 'test_feat';

    it('calls next', () => {
        const { invoke, next } = create(apiMiddleware);
        const action = { type: 'TEST' };

        invoke(action);

        expect(next).toHaveBeenCalledWith(action);
    });

    it('dispatch api success on request success', () => {
        const { invoke, next, store } = create(apiMiddleware);
        const action = actions.apiRequest({ body: null, method: 'GET', url: '', feature });
        const data = { test: 'test data' };

        jest.mock('axios');

        invoke(action);

        expect(next).toHaveBeenCalledWith(action);

        expect(store.dispatch).toHaveBeenCalledWith(actions.apiSuccess({
            response: axios.mockResolvedValue({ data }),
            feature,
        }));
    });
});

create() is just a function that I've taken from this part of the doc. It permit me to mock dispatch, getState and next.

Obviously this doesn't worked but I'm sure there's a way.

1

1 Answers

2
votes

Here is the unit test solution:

api.middleware.ts:

import { Middleware } from 'redux';
import axios from 'axios';
import { API_REQUEST } from './actionTypes';
import { apiSuccess, apiError } from './actionCreator';

export const apiMiddleware: Middleware = ({ dispatch }) => (next) => async (action): Promise<void> => {
  next(action);

  if (action.type.includes(API_REQUEST)) {
    const body = action.payload;
    const { url, method, feature } = action.meta;

    try {
      const response = await axios({ method, url, data: body });
      dispatch(apiSuccess({ response, feature }));
    } catch (error) {
      console.error(error);
      dispatch(apiError({ error, feature }));
    }
  }
};

actionTypes.ts:

export const API_REQUEST = 'API_REQUEST';
export const API_REQUEST_SUCCESS = 'API_REQUEST_SUCCESS';
export const API_REQUEST_FAILURE = 'API_REQUEST_FAILURE';

actionCreator.ts:

import { API_REQUEST_SUCCESS, API_REQUEST_FAILURE } from './actionTypes';

export function apiSuccess(data) {
  return {
    type: API_REQUEST_SUCCESS,
    ...data,
  };
}
export function apiError(data) {
  return {
    type: API_REQUEST_FAILURE,
    ...data,
  };
}

api.middleware.test.ts:

import { apiMiddleware } from './api.middleware';
import axios from 'axios';
import { MiddlewareAPI } from 'redux';
import { API_REQUEST, API_REQUEST_SUCCESS, API_REQUEST_FAILURE } from './actionTypes';

jest.mock('axios', () => jest.fn());

describe('59754838', () => {
  afterEach(() => {
    jest.clearAllMocks();
  });
  describe('#apiMiddleware', () => {
    describe('Unit test', () => {
      it('should dispatch api success action', async () => {
        const store: MiddlewareAPI = { dispatch: jest.fn(), getState: jest.fn() };
        const next = jest.fn();
        const action = {
          type: API_REQUEST,
          payload: {},
          meta: { url: 'http://localhost', method: 'get', feature: 'feature' },
        };
        const mResponse = { name: 'user name' };
        (axios as jest.Mocked<any>).mockResolvedValueOnce(mResponse);
        await apiMiddleware(store)(next)(action);
        expect(next).toBeCalledWith(action);
        expect(axios).toBeCalledWith({ method: action.meta.method, url: action.meta.url, data: action.payload });
        expect(store.dispatch).toBeCalledWith({
          type: API_REQUEST_SUCCESS,
          response: mResponse,
          feature: action.meta.feature,
        });
      });

      it('should dispatch api error action', async () => {
        const store: MiddlewareAPI = { dispatch: jest.fn(), getState: jest.fn() };
        const next = jest.fn();
        const action = {
          type: API_REQUEST,
          payload: {},
          meta: { url: 'http://localhost', method: 'get', feature: 'feature' },
        };
        const mError = new Error('network error');
        (axios as jest.Mocked<any>).mockRejectedValueOnce(mError);
        await apiMiddleware(store)(next)(action);
        expect(next).toBeCalledWith(action);
        expect(axios).toBeCalledWith({ method: action.meta.method, url: action.meta.url, data: action.payload });
        expect(store.dispatch).toBeCalledWith({
          type: API_REQUEST_FAILURE,
          error: mError,
          feature: action.meta.feature,
        });
      });
    });
  });
});

Unit test results with coverage report:

 PASS  src/stackoverflow/59754838/api.middleware.test.ts (11.206s)
  59754838
    #apiMiddleware
      Unit test
        ✓ should dispatch api success action (21ms)
        ✓ should dispatch api error action (23ms)

  console.error src/stackoverflow/59754838/api.middleware.ts:3460
    Error: network error
        at /Users/ldu020/workspace/github.com/mrdulin/jest-codelab/src/stackoverflow/59754838/api.middleware.test.ts:42:24
        at step (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/src/stackoverflow/59754838/api.middleware.test.ts:33:23)
        at Object.next (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/src/stackoverflow/59754838/api.middleware.test.ts:14:53)
        at /Users/ldu020/workspace/github.com/mrdulin/jest-codelab/src/stackoverflow/59754838/api.middleware.test.ts:8:71
        at new Promise (<anonymous>)
        at Object.<anonymous>.__awaiter (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/src/stackoverflow/59754838/api.middleware.test.ts:4:12)
        at Object.<anonymous> (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/src/stackoverflow/59754838/api.middleware.test.ts:34:46)
        at Object.asyncJestTest (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:102:37)
        at resolve (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-jasmine2/build/queueRunner.js:43:12)
        at new Promise (<anonymous>)
        at mapper (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-jasmine2/build/queueRunner.js:26:19)
        at promise.then (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-jasmine2/build/queueRunner.js:73:41)
        at process._tickCallback (internal/process/next_tick.js:68:7)

-------------------|----------|----------|----------|----------|-------------------|
File               |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
-------------------|----------|----------|----------|----------|-------------------|
All files          |      100 |       50 |      100 |      100 |                   |
 actionCreator.ts  |      100 |      100 |      100 |      100 |                   |
 actionTypes.ts    |      100 |      100 |      100 |      100 |                   |
 api.middleware.ts |      100 |       50 |      100 |      100 |                 9 |
-------------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        12.901s

Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/59754838