使用Jest测试Redux thunk中的调度操作

时间:2018-01-09 16:40:39

标签: reactjs redux fetch jestjs redux-thunk

我对Jest很陌生,并且无可否认地测试异步代码......

我使用的是一个简单的Fetch助手:

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);
        });
    }

并按照以下方式实施:

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));
        });
    };
}

但是,我想测试在正确的情况下以正确的顺序触发正确的调度。

sinon.spy()过去很容易,但我无法弄清楚如何在Jest中复制它。理想情况下,我希望我的测试看起来像这样:

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


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

提前感谢您的任何帮助或建议!

4 个答案:

答案 0 :(得分:8)

redux文档有一个很棒的article on testing async action creators

  

对于使用Redux Thunk或其他中间件的异步操作创建者,最好完全模拟Redux存储以进行测试。您可以使用redux-mock-store将中间件应用于模拟商店。您还可以使用fetch-mock模拟HTTP请求。

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)
    })
  })
})

他们的方法不是使用jest(或sinon)进行间谍,而是使用模拟存储并断言调度的操作。这样做的好处是能够处理thunks调度thunk,这对间谍来说很难。

这一切都直接来自文档,但是如果你想让我为你的thunk创建一个例子,请告诉我。

答案 1 :(得分:7)

对于使用Redux Thunk或其他中间件的异步操作创建者,最好完全模拟Redux存储以进行测试。您可以使用nock将中间件应用于模拟商店。为了模拟HTTP请求,您可以使用store.getActions()

根据https://github.com/jackpal/Android-Terminal-Emulator/wiki/Android-Shell-Command-Reference,您需要在请求结束时调用mockStore(getState?: Object,Function) => store: Function来测试异步操作,您可以配置您的测试,如

  

store.dispatch(action) => action返回一个   配置的模拟商店的实例。如果您想重置商店   每次测试后,你都应该调用这个函数。

     

store.getState() => state: Object通过以下方式调度操作   模拟商店。该操作将存储在实例内的数组中   并执行。

     

store.getActions() => actions: Array返回模拟的状态   存储

     

store.clearActions()返回模拟的动作   存储

     

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); }); }); 清除存储的操作

您可以编写像

这样的测试操作
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

    }
}

PS 保持模仿商店在模拟操作被触发时不会自我更新,并且如果您依赖于上一个操作之后的更新数据然后你需要编写自己的实例,如

mockStore

然后配置 const store = mockStore(getMockStore); 之类的

function Repo.get_by/2 is undefined (module Repo is not available)

希望它有所帮助。另请参阅有关测试异步操作创建者的redux文档中的 redux-mock-store documentation

答案 2 :(得分:2)

如果您使用jest.fn()模拟调度功能,则只需访问dispatch.mock.calls即可获得对存根的所有调用。

  const dispatch = jest.fn();
  actions.yourAction()(dispatch);

  expect(dispatch.mock.calls.length).toBe(1);

  expect(dispatch.mock.calls[0]).toBe({
    type: SET_DATA,
    value: {...},
  });

答案 3 :(得分:0)

在我的回答中,我使用的是axios而不是fetch,因为我对获取承诺没有多少经验,这对您的问题无关紧要。我个人对axios非常满意。
查看我在下面提供的代码示例:

// 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,
    });
  });
});

这里我模拟了fetch helper来返回Resolved promise来测试成功部分并拒绝承诺来测试失败的api调用。您可以将参数传递给它们以验证响应。
您可以像这样实施getSomeData

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));
      })
  }
}

我希望这能解决你的问题。如果您需要任何澄清,请发表评论 P.S你可以通过查看上面的代码看到为什么我更喜欢axios而不是fetch,从大量的承诺中解脱出来!
如需进一步阅读,请参阅:https://medium.com/@thejasonfile/fetch-vs-axios-js-for-making-http-requests-2b261cdd3af5

相关问题