4

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

sineverba
  • 5,059
  • 7
  • 39
  • 84
  • You're probably looking for https://stackoverflow.com/questions/45081717/how-can-i-test-thunk-actions-with-jest – timotgl Oct 13 '21 at 13:59
  • @timotgl my test is very similar to linked answer. I get only one of the two actions (but they are dispatched, I can get them from real app usage) – sineverba Oct 13 '21 at 16:28
  • You're right, the question I linked has terrible answers that don't work. You need to make the test itself async. That means declaring the function you pass to it() as `async` and then use `await` inside (when dispatching the thunk action). Or alternatively, you return a promise in the it-function and assert on the resolved value in the .then handler. Jest waits for these to resolve by default. – timotgl Oct 14 '21 at 11:29
  • Can you post me an example? I tried without luck, I'm updating the answer... – sineverba Oct 14 '21 at 17:29
  • posting an example from an older project I dug up below – timotgl Oct 19 '21 at 11:06

2 Answers2

1

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.

nima
  • 7,796
  • 12
  • 36
  • 53
  • 1
    It seems working, only in await I have ALL actions. But not an issue, really. ``` await waitFor(() => { expect(store.getActions()).toEqual([{ type: "TRY_FETCH_PING_ITEMS", }, { type: "FETCH_PING_ITEMS_SUCCEEDED", data: dataFetchItems, }]); }); ``` – sineverba Oct 20 '21 at 10:50
  • 1
    As you said it's not an issue at all, in await block, you can get an array of actions since you waiting for all the expectations to pass in this block. thanks for your reply and attention Sir, @sineverba – nima Oct 20 '21 at 11:25
  • @novonimo This might be "a" solution, but not "the" solution. You can simply return a promise in the thunk and make it `await`able. No need for `waitFor` then. – timotgl Oct 21 '21 at 10:36
  • yes of course @timotgl. there are might be many other solutions but with the current implementation (without returning a promise in thunk), we need to `waitFor` method. – nima Oct 21 '21 at 10:48
  • also, I didn't say it's the best solution, "the problem" and "the solution" are just a title to separate the context. hope to clarify the issue and solve the misunderstanding. @timotgl – nima Oct 21 '21 at 10:56
0

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));
    });
});
timotgl
  • 2,865
  • 1
  • 9
  • 19
  • 1
    I made as you made it (see my questions). I cannot get second one action (the success, to understand), only the first one. – sineverba Oct 19 '21 at 11:54
  • @sineverba Have you tried debugging if the success action actually gets dispatched? Something in your setup must be different. – timotgl Oct 19 '21 at 12:27
  • Yes, action is dispached. 'Cause app works and I can see action on REDUX toolbox on Chrome and in coverage report. – sineverba Oct 19 '21 at 13:33
  • 1
    @sineverba I meant in the test environment. Put a console.log next to the code that dispatches the success action, and make the test wait a bit longer with something like `await new Promise((r) => setTimeout(r, 2000));` before asserting. – timotgl Oct 19 '21 at 14:20
  • Please, see my code. I have placed a bit of console log yet. – sineverba Oct 19 '21 at 16:56
  • By the way, I have placed the repository. If you want, you can clone it and test directly, if this can be helpful – sineverba Oct 19 '21 at 16:57
  • 1
    @sineverba I cloned your repo and ran the tests. There is simply a `return` missing in `fetchItems()`. If you return a promise from the thunk, you can `await` it and everything works as expected. Will comment on your github repo. `waitFor` is meant for tougher cases like React components doing async stuff in effects etc.. – timotgl Oct 21 '21 at 10:35
  • Just tested your advice. Yes, it works. I have two methods, now. Thank you to both... – sineverba Oct 21 '21 at 16:03