Redux-mock-store: Cannot get all dispatched actions into mocked store

Viewed 657

I have a CrudActions.js class:

export default class CrudActions {

  constructor(entity, api) {
    this.setEntity(entity);
    this.setApi(api);
  }

  setEntity(entity) {
    this.entity = entity.toUpperCase();
  }

  setApi(api) {
    this.api = api;
  };

  getEntity() {
    return this.entity;
  };

  getApi() {
    return this.api;
  };

  fetchItems() {
    return dispatch => {
      dispatch(
        {
          type: `TRY_FETCH_${this.getEntity()}_ITEMS`,
        }
      );
      this.getApi()
      .fetchItems()
      .then(data => {
        dispatch({
          type: `FETCH_${this.getEntity()}_ITEMS_SUCCEEDED`,
          data
        });
      })
      .catch(error => {
        dispatch({
          type: `FETCH_${this.getEntity()}_ITEMS_FAILED`,
          error,
        });
      })
    }
  };

}

I extend it with a new class (one class for every route)

import { instance as api } from "../../api/app/Ping";
import CrudActions from "../base/CrudActions";

export default class PingActions extends CrudActions {
  constructor() {
    super("ping", api);
  }
}

export const actions = new PingActions();

I want put under test fetchItems and test that right actions are dispatched.

So, in a Ping.test.js:

import { actions as pingActions } from "../../../../utils/actions/app/PingActions";
import { axiosInstance } from "../../../../utils/api/base/axiosInstance";
import MockAdapter from "axios-mock-adapter";
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';

const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);

const entity = 'ping';
const baseUrl = '/ping';
const dataFetchItems = [
  {
    app_version: "9.8.7"
  }
];

describe('Test PingActions', () => {

  let mock;
  let store;

  beforeEach(() => {
    store = mockStore({
      ping: {
        items: dataFetchItems
      }
    })
  })

  beforeAll(() => {
    mock = new MockAdapter(axiosInstance);
  });

  afterEach(() => {
    mock.reset();
  });


  it ('Test can dispatch success actions', () => {
    mock.onGet('http://localhost:8000/api/v1'+baseUrl).reply(200, dataFetchItems);
    store.dispatch(pingActions.fetchItems());
    console.log(store.getActions());
    expect(store.getActions()).toContainEqual({
      type: "TRY_FETCH_PING_ITEMS",
    });
  });

  it ('Test can dispatch fail actions', () => {
    mock.onGet('http://localhost:8000/api/v1'+baseUrl).reply(401);
    store.dispatch(pingActions.fetchItems());
    console.log(store.getActions());
    expect(store.getActions()).toContainEqual({
      type: "TRY_FETCH_PING_ITEMS",
    });
  });
});

With these tests I can cover both case: "TRY_FETCH_PING_ITEMS" and "FETCH_PING_ITEMS_SUCCEEDED" (I see it from coverage).

I cannot understand how get FETCH_PING_ITEMS_SUCCEEDED or FETCH_PING_ITEMS_FAILED actions in store.getActions().

store.getActions() has only TRY_FETCH_PING_ITEMS inside:

 PASS  src/__tests__/utils/actions/app/PingActions.test.js
  ● Console

    console.log
      [ { type: 'TRY_FETCH_PING_ITEMS' } ]

      at Object.<anonymous> (src/__tests__/utils/actions/app/PingActions.test.js:46:13)

    console.log
      [ { type: 'TRY_FETCH_PING_ITEMS' } ]

      at Object.<anonymous> (src/__tests__/utils/actions/app/PingActions.test.js:55:13)

I made a new test, without luck:

it ('Test can dispatch success actions', async () => {
    mock.onGet('http://localhost:8000/api/v1'+baseUrl).reply(200, dataFetchItems);
    await store.dispatch(pingActions.fetchItems());
    console.log(store.getActions());
    expect(store.getActions()).toContainEqual({
      type: "TRY_FETCH_PING_ITEMS",
    });
  });

But I get...

 PASS  src/__tests__/utils/actions/app/PingActions.test.js
  ● Console

    console.log
      [ { type: 'TRY_FETCH_PING_ITEMS' } ]

      at Object.<anonymous> (src/__tests__/utils/actions/app/PingActions.test.js:46:13)

(I miss, every time, the FETCH_PING_ITEMS_SUCCEEDED)

Another test:

it ('Test can dispatch success actions', () => {
    mock.onGet('http://localhost:8000/api/v1'+baseUrl).reply(200, dataFetchItems);
    return store.dispatch(pingActions.fetchItems()).then(data => console.log(data));
  });

But I get

TypeError: Cannot read property 'then' of undefined

Or also:

it ('Test can dispatch success actions', () => {
    mock.onGet('http://localhost:8000/api/v1'+baseUrl).reply(200, dataFetchItems);
    const data = pingActions.fetchItems().then(data => console.log(data));
  });

I get

TypeError: _PingActions.actions.fetchItems(...).then is not a function

The Github Repository: https://github.com/sineverba/body-measurement-frontend

2 Answers

A few bit changes will make it work.

The Problem

You expect that FETCH_PING_ITEMS_SUCCEEDED or FETCH_PING_ITEMS_FAILED actions should be dispatched after the TRY_FETCH_PING_ITEMS action. since both success and failure cases are a promise, so they need to be processed in the proper way (nicely implemented in the CrudActions with then/catch block) but you need to handle these asynchronous actions also in your test case after dispatching the TRY_FETCH_PING_ITEMS.

The Solution

from React testing library documentation:

When in need to wait for any period of time you can use waitFor, to wait for your expectations to pass.

import {waitFor} from '@testing-library/react'

it('Test can dispatch success actions', async () => {
    mock.onGet('http://localhost:8000/api/v1' + baseUrl).reply(200);
    store.dispatch(pingActions.fetchItems());

    expect(store.getActions()).toContainEqual({
      type: "TRY_FETCH_PING_ITEMS"
    })

    await waitFor(() => {
        expect(store.getActions()).toContainEqual({
          type: "FETCH_PING_ITEMS_SUCCEEDED",
        })
    })
})

You can also put the fetch ping expectation in the waitFor callback.

await waitFor(() => {
    expect(store.getActions()).toContainEqual({
      type: "TRY_FETCH_PING_ITEMS"
    })

    expect(store.getActions()).toContainEqual({
      type: "FETCH_PING_ITEMS_SUCCEEDED",
    })
})

Note: Don't forget to add async keyword before the callback function in the it method.

Note: For failure case, do the as same as the success case.

Here's a generic example of testing a thunk, hope this helps.

import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';

import getApiClient from './api-client';
import { initialState } from './reducer';
import * as Actions from './actions';

jest.mock('api-client');
jest.mock('actions');

const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);

describe('getStuff async action', () => {
    it('should request data via API', async () => {
        const store = mockStore({ stuff: initialState });

        // Mock api client method so we're not sending out actual http requests
        getApiClient.mockImplementationOnce(() => ({
            getStuff: async () => [{ id: '1' }],
        }));

        // Don't remember if or why this part was necessary, but it was working regardless :D
        const Actions = require('./actions');

        // Describe the expected sequence of actions dispatched from the thunk
        const expected = [
            { type: 'STUFF/REQUEST' },
            {
                type: 'STUFF/SUCCESS',
                payload: { items: [{ id: '1' }] },
            },
        ];

        // Dispatch the thunk and wait for it to complete
        await store.dispatch(Actions.getStuff('1'));

        const dispatchedActions = store.getActions();        
        expect(dispatchedActions[0]).toEqual(expected[0]);
        expect(dispatchedActions[1].payload).toEqual(expect.objectContaining(expected[1].payload));
    });
});
Related