【问题标题】:Testing dispatched actions in Redux thunk with Jest使用 Jest 在 Redux thunk 中测试分派的操作
【发布时间】:2018-06-18 18:16:25
【问题描述】:

我对 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: {...},
});

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

【问题讨论】:

  • redux 应该避免连续调度多个动作。您有dispatch(setData(data)); dispatch(setLoading(false));,它将触发 2 次存储更改和 2 次渲染。如果您将其组合成一个操作,并将该操作的加载状态设置为 false,那么您的应用中将只有 1 次重新渲染。

标签: reactjs redux fetch jestjs redux-thunk


【解决方案1】:

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)来监视,而是使用模拟存储并断言调度的操作。这具有能够处理 thunk 调度 thunk 的优势,这对于间谍来说很难做到。

这一切都直接来自文档,但如果您希望我为您的 thunk 创建一个示例,请告诉我。

【讨论】:

  • 您是否有任何关于 thunk 调度 thunk 的文档的链接?我似乎看不到他们是否被派遣。
  • 手头没有。我在测试 thunk 时看到的一个常见问题是期望调用是在异步调用完成之前进行的(例如,它们不返回或等待承诺),因此请仔细检查这类事情。如果不是这样,最好用更多细节打开您自己的问题。
  • 我能得到一些帮助吗stackoverflow.com/questions/61948582/…
  • 对于我们这些想要通过与实际 API 调用集成而不以任何方式模拟它们来进行实际集成测试的人来说?
【解决方案2】:

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

根据redux-mock-store documentation,你需要在请求结束时调用store.getActions()来测试异步动作,你可以像这样配置你的测试

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

PS 请注意,当模拟操作被触发时,模拟存储不会自行更新,如果您依赖上一个操作之后的更新数据用于下一个操作那么你需要编写你自己的实例

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

    }
}

然后配置mockStore就好了

 const store = mockStore(getMockStore);

希望对您有所帮助。还可以在 redux 文档中查看 this 以测试异步操作创建者

【讨论】:

【解决方案3】:

如果您使用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: {...},
  });

【讨论】:

  • 感谢@Canastro,但这似乎对我不起作用。当我像actions.yourAction()(dispatch); 那样格式化调度时,它会引发错误。我试过这样做dispatch(actions.urnSearch(id));,当我登录dispatch.mock.calls[0]时,它只会给我[ [Function] ]
  • 我能得到一些帮助吗stackoverflow.com/questions/61948582/…
  • 如果您从动作创建者那里分派其他动作,这将不起作用
【解决方案4】:

在我的回答中,我使用的是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 助手以返回已解决的承诺以测试成功部分并拒绝承诺以测试失败的 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

【讨论】:

猜你喜欢
  • 2017-10-14
  • 2017-12-18
  • 2017-03-03
  • 1970-01-01
  • 2017-02-01
  • 2023-01-09
  • 1970-01-01
  • 2017-06-18
  • 2017-10-15
相关资源
最近更新 更多