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

Reactjs 使用Jest在Redux thunk中测试已调度的操作,reactjs,redux,fetch,jestjs,redux-thunk,Reactjs,Redux,Fetch,Jestjs,Redux Thunk,我对开玩笑还不太熟悉,而且我不擅长测试异步代码 我使用了一个简单的Fetch助手: export function fetchHelper(url, opts) { return fetch(url, options) .then((response) => { if (response.ok) { return Promise.resolve(response); }

我对开玩笑还不太熟悉,而且我不擅长测试异步代码

我使用了一个简单的
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()
,这过去非常容易,但我不太明白如何在玩笑中复制它。理想情况下,我希望我的测试看起来像这样:

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


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

提前感谢您的帮助或建议

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

redux文档具有很好的功能:


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

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


这些都是直接从文档中获得的,但是如果您想让我为您的thunk创建一个示例,请告诉我。

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

根据,您需要在测试异步操作的请求结束时调用
store.getActions()
,您可以像

mockStore(getState?:对象,函数)=>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文档中关于测试异步动作创建者的,在我的回答中,我使用的是
axios
,而不是
fetch
,因为我在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助手返回解析的promise to test success部分,并拒绝promise to test failed 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,从而使您免于许下许多多承诺
关于它的进一步阅读,您可以参考:

谢谢@Canastro,但这似乎对我不起作用。当我格式化调度时,就像你有
actions.yourAction()(调度)一样它抛出一个错误。我试着这样做
dispatch(actions.urnSearch(id))当我记录
dispatch.mock.calls[0]
时,它只会给我
[[Function]]
我可以在这方面获得一些帮助吗?如果你从动作创建者那里调度其他动作,这是行不通的。使用redux应该避免在一行中调度多个动作。您有
dispatch(setData(data));调度(设置加载(假))将触发2个存储更改和2个渲染。如果您将其合并到单个操作中,并将该操作的加载状态设置为false,那么您的应用程序中只有1个重新渲染。您是否有指向thunks调度thunks的任何文档的链接?我看不出他们是否被派去了,手头没有。我在测试thunks时看到的一个常见问题是,expect调用是在异步调用完成之前进行的(例如,它们不返回或等待承诺),所以请仔细检查这类事情。如果不是这样的话,你最好用更多的细节来回答你自己的问题。我能在这方面得到一些帮助吗?我能在这方面得到一些帮助吗
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));
      })
  }
}