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

Dan*_*anV 10 fetch reactjs jestjs redux 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);
        });
    }
Run Code Online (Sandbox Code Playgroud)

并像这样实现它:

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));
        });
    };
}
Run Code Online (Sandbox Code Playgroud)

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

这曾经很容易用a 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: {...},
});
Run Code Online (Sandbox Code Playgroud)

在此先感谢您的任何帮助或建议!

Mic*_*per 9

redux文档有关于测试异步动作创建者的精彩文章:

对于使用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)
    })
  })
})
Run Code Online (Sandbox Code Playgroud)

他们的方法不是使用jest(或sinon)进行间谍,而是使用模拟存储并断言调度的操作.这具有能够处理thunks调度thunk的优点,这对于间谍来说是非常困难的.

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


fre*_*ett 8

截至 2023 年 1 月的相关答案

2018 年的许多有用答案现在已经过时,截至 2023 年的答案是避免嘲笑商店,而是使用真正的商店,更喜欢集成测试(仍然使用笑话)而不是单元测试。

更新后的官方 Redux 测试文档中的一些亮点:

更喜欢编写所有东西一起工作的集成测试。对于使用 Redux 的 React 应用程序,渲染一个包含正在测试的组件的真实存储实例。与正在测试的页面的交互应该使用真正的 Redux 逻辑,并模拟 API 调用,这样应用程序代码就不必更改,并断言 UI 已适当更新。

不要尝试模拟选择器函数或 React-Redux 钩子!从库中模拟导入很脆弱,并且无法让您确信实际的应用程序代码正在运行。

接下来说明了如何实现这一点,其renderWithProvider功能详细信息请参见此处

为了对此进行推理,它链接到的文章包含以下引用,解释了 redux 测试最佳实践思想的演变:

我们的文档一直教导“隔离”方法,这对于减速器和选择器尤其有意义。“整合”方法只占少数。

但是,RTL 和 Kent C Dodds 极大地改变了 React 生态系统中测试的思维方式和方法。我现在看到的模式是关于“集成”式测试 - 大量代码,一起工作,就像它们在真实应用程序中使用一样。


Shu*_*tri 6

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

根据redux-mock-store文档,您需要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);
    });
});
Run Code Online (Sandbox Code Playgroud)

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

    }
}
Run Code Online (Sandbox Code Playgroud)

然后配置mockStore类似

 const store = mockStore(getMockStore);
Run Code Online (Sandbox Code Playgroud)

希望能帮助到你.还要检查对测试异步操作的创造者终极版文档中


Can*_*tro 4

如果您使用 模拟调度函数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: {...},
  });
Run Code Online (Sandbox Code Playgroud)