reactjs 使用Jest测试Redux thunk中的已调度动作

bqucvtff  于 2023-01-08  发布在  React
关注(0)|答案(5)|浏览(158)

我对Jest还是个新手,而且我承认我不是测试异步代码的Maven...
我使用了一个简单的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: {...},
});

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

5t7ly7z5

5t7ly7z51#

截至2018年1月的答复

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

他们的方法不是使用jest(或sinon)进行间谍活动,而是使用模拟存储并Assert调度的操作。这样做的好处是能够处理thunk调度thunk,而这对于间谍活动来说可能非常困难。
这些都是直接从文档中得到的,但是如果你想让我为你的thunk创建一个例子,请告诉我。

    • (这句话从2023年1月起不再出现在文章中,建议也发生了巨大变化,请参阅对此答案的评论以了解更多信息)*
ugmeyewa

ugmeyewa2#

截至2018年1月的答复

对于使用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);
    });
});
    • P.S.**请记住,模拟存储不会在模拟操作启动时自动更新,如果您依赖于上一个操作之后更新的数据用于下一个操作,则需要编写自己的示例,如下所示
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关于测试异步操作创建者的内容

uubf1zoe

uubf1zoe3#

如果要使用jest.fn()模拟dispatch函数,只需访问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: {...},
  });
h5qlskok

h5qlskok4#

在我的回答中,我使用axios而不是fetch,因为我在获取承诺方面没有太多经验,这对你的问题应该没有关系。
请看下面我提供的代码示例:

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

这里我模拟了获取助手返回Resolved promise到测试成功部分,并拒绝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));
      })
  }
}

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

u7up0aaq

u7up0aaq5#

截至2023年1月的相关答案

2018年的许多有用的答案现在已经过时了,2023年的答案是避免嘲笑商店,而是使用真实的的商店,更喜欢集成测试(仍然使用jest)而不是单元测试。
更新后的official Redux testing documentation中的一些亮点:
喜欢编写集成测试,让所有东西都协同工作。对于使用Redux的React应用,使用真实的的商店示例渲染 Package 被测组件。与被测页面的交互应使用真实的Redux逻辑,模拟API调用,以便应用代码不必更改,并AssertUI已适当更新。

不要尝试模拟选择器函数或React-Redux钩子!模拟从库导入是脆弱的,并且不能给予您确信实际的应用代码正在工作。

它继续说明如何实现这一点,并在这里详细介绍renderWithProvider函数。
The article it links to for reasoning on this,包括以下引文,解释了redux测试最佳实践思想的演变:
我们的医生一直在教授“隔离”方法,这对简化者和选择者来说尤其有意义,而“整合”方法则是少数。
但是,RTL和肯特C Dodds彻底改变了React生态系统中的测试思维和方法,我现在看到的模式是关于“集成”风格的测试--大块代码一起工作,就像它们在真实的应用中使用的那样。

相关问题