Testing dispatched actions in Redux thunk with Jest

前端 未结 4 767
栀梦
栀梦 2020-12-15 07:33

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 fetchHe         


        
相关标签:
4条回答
  • 2020-12-15 08:07

    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.

    0 讨论(0)
  • 2020-12-15 08:14

    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

    0 讨论(0)
  • 2020-12-15 08:22

    If you're mocking the dispatch function with jest.fn(), you can just access dispatch.mock.calls to get all the calls made to your stub.

      const dispatch = jest.fn();
      actions.yourAction()(dispatch);
    
      expect(dispatch.mock.calls.length).toBe(1);
    
      expect(dispatch.mock.calls[0]).toBe({
        type: SET_DATA,
        value: {...},
      });
    
    0 讨论(0)
  • 2020-12-15 08:22

    In my answer I am using axios instead of fetch as I don't have much experience on fetch promises, that should not matter to your question. I personally feel very comfortable with axios.
    Look at the code sample that I am providing below:

    // apiCalls.js
    const fetchHelper = (url) => {
      return axios.get(url);
    }
    
    
    import * as apiCalls from './apiCalls'
    describe('getSomeData', () => {
      it('should dispatch SET_LOADING_STATE on start of call', async () => {
        spyOn(apiCalls, 'fetchHelper').and.returnValue(Promise.resolve());
        const mockDispatch = jest.fn();
    
        await getSomeData()(mockDispatch);
    
        expect(mockDispatch).toHaveBeenCalledWith({
          type: SET_LOADING_STATE,
          value: true,
        });
      });
    
      it('should dispatch SET_DATA action on successful api call', async () => {
        spyOn(apiCalls, 'fetchHelper').and.returnValue(Promise.resolve());
        const mockDispatch = jest.fn();
    
        await getSomeData()(mockDispatch);
    
        expect(mockDispatch).toHaveBeenCalledWith({
          type: SET_DATA,
          value: { ...},
        });
      });
    
      it('should dispatch SET_FAIL action on failed api call', async () => {
        spyOn(apiCalls, 'fetchHelper').and.returnValue(Promise.reject());
        const mockDispatch = jest.fn();
    
        await getSomeData()(mockDispatch);
    
        expect(mockDispatch).toHaveBeenCalledWith({
          type: SET_FAIL,
        });
      });
    });
    

    Here I am mocking the fetch helper to return Resolved promise to test success part and reject promise to test failed api call. You can pass arguments to them to validate on response also.
    You can implement getSomeData like this:

    const getSomeData = () => {
      return (dispatch) => {
        dispatch(setLoading(true));
        return fetchHelper('http://datasource.com/')
          .then(response => {
            dispatch(setData(response.data));
            dispatch(setLoading(false));
          })
          .catch(error => {
            dispatch(setFail());
            dispatch(setLoading(false));
          })
      }
    }
    

    I hope this solves your problem. Please comment, if you need any clarification.
    P.S You can see by looking at above code why I prefer axios over fetch, saves you from lot of promise resolves!
    For further reading on it you can refer: https://medium.com/@thejasonfile/fetch-vs-axios-js-for-making-http-requests-2b261cdd3af5

    0 讨论(0)
提交回复
热议问题