Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing dispatched actions in Redux thunk with Jest

I'm quite new to Jest and admittedly am no expert at testing async code...

I have a simple Fetch helper I use:

export function fetchHelper(url, opts) {
    return fetch(url, options)
        .then((response) => {
            if (response.ok) {
                return Promise.resolve(response);
            }

            const error = new Error(response.statusText || response.status);
            error.response = response;

            return Promise.reject(error);
        });
    }

And implement it like so:

export function getSomeData() {
    return (dispatch) => {
        return fetchHelper('http://datasource.com/').then((res) => {
            dispatch(setLoading(true));
            return res.json();
        }).then((data) => {
            dispatch(setData(data));
            dispatch(setLoading(false));
        }).catch(() => {
            dispatch(setFail());
            dispatch(setLoading(false));
        });
    };
}

However I want to test that the correct dispatches are fired in the correct circumstances and in the correct order.

This used to be quite easy with a sinon.spy(), but I can't quite figure out how to replicate this in Jest. Ideally I'd like my test to look something like this:

expect(spy.args[0][0]).toBe({
  type: SET_LOADING_STATE,
  value: true,
});


expect(spy.args[1][0]).toBe({
  type: SET_DATA,
  value: {...},
});

Thanks in advance for any help or advice!

like image 494
DanV Avatar asked Jan 09 '18 16:01

DanV


People also ask

Can we dispatch action from thunk?

Dispatching Actions​Thunks have access to the dispatch method. This can be used to dispatch actions, or even other thunks.

How do you test thunk action?

We use store. getActions() to identify the actions that our thunk has dispatched when it was called. expectedActions is an array which holds the list of actions that the thunk should have called. We pass this to expect() and verify if the actions that the thunk has dispatched are the same as those we had anticipated.

What is Dispatch in Redux thunk?

dispatch: It is a method used to dispatch actions, that can be received by reducers. 2. getState: It gives access to store inside the thunk function. A thunk function may contain any arbitrary logic, sync, or async, and can call dispatch or getState at any time.


2 Answers

The redux docs have a great article on testing async action creators:

For async action creators using Redux Thunk or other middleware, it's best to completely mock the Redux store for tests. You can apply the middleware to a mock store using redux-mock-store. You can also use fetch-mock to mock the HTTP requests.

import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import * as actions from '../../actions/TodoActions'
import * as types from '../../constants/ActionTypes'
import fetchMock from 'fetch-mock'
import expect from 'expect' // You can use any testing library

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

describe('async actions', () => {
  afterEach(() => {
    fetchMock.reset()
    fetchMock.restore()
  })

  it('creates FETCH_TODOS_SUCCESS when fetching todos has been done', () => {
    fetchMock
      .getOnce('/todos', { body: { todos: ['do something'] }, headers: { 'content-type': 'application/json' } })


    const expectedActions = [
      { type: types.FETCH_TODOS_REQUEST },
      { type: types.FETCH_TODOS_SUCCESS, body: { todos: ['do something'] } }
    ]
    const store = mockStore({ todos: [] })

    return store.dispatch(actions.fetchTodos()).then(() => {
      // return of async actions
      expect(store.getActions()).toEqual(expectedActions)
    })
  })
})

Their approach is not to use jest (or sinon) to spy, but to use a mock store and assert the dispatched actions. This has the advantage of being able to handle thunks dispatching thunks, which can be very difficult to do with spies.

This is all straight from the docs, but let me know if you want me to create an example for your thunk.

like image 60
Michael Peyper Avatar answered Sep 17 '22 14:09

Michael Peyper


For async action creators using Redux Thunk or other middleware, it's best to completely mock the Redux store for tests. You can apply the middleware to a mock store using redux-mock-store. In order to mock the HTTP request, you can make use of nock.

According to redux-mock-store documentation, you will need to call store.getActions() at the end of the request to test asynchronous actions, you can configure your test like

mockStore(getState?: Object,Function) => store: Function Returns an instance of the configured mock store. If you want to reset your store after every test, you should call this function.

store.dispatch(action) => action Dispatches an action through the mock store. The action will be stored in an array inside the instance and executed.

store.getState() => state: Object Returns the state of the mock store

store.getActions() => actions: Array Returns the actions of the mock store

store.clearActions() Clears the stored actions

You can write the test action like

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

//Configuring a mockStore
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);

//Import your actions here
import {setLoading, setData, setFail} from '/path/to/actions';

test('test getSomeData', () => {
    const store = mockStore({});

    nock('http://datasource.com/', {
       reqheaders // you can optionally pass the headers here
    }).reply(200, yourMockResponseHere);

    const expectedActions = [
        setLoading(true),
        setData(yourMockResponseHere),
        setLoading(false)
    ];

    const dispatchedStore = store.dispatch(
        getSomeData()
    );
    return dispatchedStore.then(() => {
        expect(store.getActions()).toEqual(expectedActions);
    });
});

P.S. Keep in ming that the mock-store does't update itself when the mocked action are fired and if you are depending on the updated data after the previous action to be used in the next action then you need to write your own instance of it like

const getMockStore = (actions) => {
    //action returns the sequence of actions fired and 
    // hence you can return the store values based the action
    if(typeof action[0] === 'undefined') {
         return {
             reducer: {isLoading: true}
         }
    } else {
        // loop over the actions here and implement what you need just like reducer

    }
}

and then configure the mockStore like

 const store = mockStore(getMockStore);

Hope it helps. Also check this in redux documentation on testing async action creators

like image 22
Shubham Khatri Avatar answered Sep 20 '22 14:09

Shubham Khatri