使用条件逻辑和getState()用法测试复杂的redux thunk?

时间:2017-08-08 13:27:36

标签: reactjs unit-testing redux react-redux redux-thunk

我的应用程序中的所有逻辑都存在于动作创建者(thunk)中。大多数动作创作者'逻辑并不复杂,它由条件表达式组成,其条件是来自商店的值:如果商店中存在此值,则调度这些操作创建者,否则调度此操作。还有一些"聚合器",它们是动作创建者,通常根据某些状态值的存在来调度其他几个动作创建者;和api包装器,它有条件地用来自状态的参数调用api抽象thunk,然后处理响应。

点是,大多数人使用getState函数来获取他们自己需要的所有东西,而不是将其作为参数接收。现在,这种方法对我很有用,并且很容易使用,但是我很难对它进行测试。到目前为止,我按照这个建议编写了所有测试:https://github.com/reactjs/redux/issues/2179。基本上,在开始时我使用其他一些操作设置所需的状态,模拟获取调用,然后调度我打算测试的thunk,然后使用各种选择器检查状态。它在一次测试中同时测试多个动作,缩减器和选择器。我喜欢我的测试完全验证特定用例的事实,但我不确定这实际上是否是一个好习惯。我的主要问题是一些thunk是不可测试的,因为他们派遣了5个其他的动作创建者,我很困惑如何至少验证他们被调用,除了检查状态是否已经改变,这反过来使Promise链巨大的,并在多个测试中一遍又一遍地测试相同的功能。

我对这整个测试事项都很陌生,互联网上的所有示例都是TODO列表或其他非常简单的CRUD应用程序,这些都没有用。你如何在复杂的应用程序中进行redux测试,这些应用程序使用大量条件逻辑,而动作创建器依赖于多个状态节点?

1 个答案:

答案 0 :(得分:1)

简短回答:嘲笑。 =)

答案很长:我个人更喜欢在测试中使用尽可能多的真实代码(即没有测试双打)。但有时候它并不值得,你必须回归嘲笑。

在您描述的情况下,您可能需要/需要检查几项测试内容:

  1. 实际上已经从你正在测试的thunk中发送了一些sub-thunk。
  2. 正在测试中的thunk正确处理它发送的子thunk的结果。
  3. 正在测试中的thunk正确处理已更新的状态,该状态由已发送的子thunk更新。
  4. 根据您想要测试的上述内容的组合,可以使用不同的策略。例如,不需要检查是否调度了子thunk以防你的测试中的thunk依赖于sub-thunk的结果:只是模拟子thunk以便它返回特定的数据,这将影响thunk的行为以特定,明智的方式进行测试(有关下面代码段中的详细信息,请参阅auth mock)。

    让我们考虑以下示例来说明可能的模拟策略。想象一下,你必须实现授权功能。假设您的服务器通过http端点授权用户,并且在成功的情况下,发送授权令牌,稍后用于打开websocket连接。我们假设您按以下方式设计了该功能:

    connect thunk,它使用用户的登录名和密码发送auth子thunk,然后通过http发送给定的凭据。当服务器响应auth thunk将收到的令牌存储在store中时(仅出于说明的原因)并解决。当auth结算时,connect会发送otherStuff thunk,这将使用令牌执行其他一些操作。最后connect通过wsApi打开套接字连接。

    // =======  connect.js ======= 
    
    import { auth, getToken } from './auth';
    import * as wsApi from './ws';
    import { otherStuff } from './other-stuff';
    
    export const connect = (login, password) => (dispatch, getState) => {
      // ...
    
      return dispatch(auth(login, password))
        .then(() => {
          const token = getToken(getState());
          dispatch(otherStuff(token));
          wsApi.connect(token);
        });
    
      // ...
    };
    
    
    // =======  auth.js ======= 
    
    import * as httpApi from './http';
    
    const saveToken = token => ({ type: 'auth/save-token', payload: token });
    
    export const auth = (login, password) =>
      dispatch =>
      httpApi.login(login, password)
      .then(token => dispatch(saveToken(token)));
    
    export const getToken = state => state.auth.token;
    
    export default (state = {}, action) => action.type === 'auth/save-token' ? { token: action.payload } : state;
    
    
    // ======= other-stuff.js ======= 
    
    export const otherStuff = token => (dispatch) => {
      // ...
    };
    

    我们要做的是模拟两个thunk:authotherStuffconnect高度依赖于auth,因此我们会确保仅通过检查auth行为来调用connect,具体取决于我们传递给auth的模拟行为。 otherStuff的情况稍微复杂一些。除了实现自定义中间件之外,没有办法检查它是否实际已分派,后者将记录所有已分派的操作。总而言之,测试将如下所示(我使用jest进行模拟):

    import { createStore, applyMiddleware, combineReducers } from 'redux';
    import thunk from 'redux-thunk';
    import { connect } from './connect';
    import { auth, getToken } from './auth';
    import { otherStuff } from './other-stuff';
    import * as wsApi from './ws';
    const authReducer = require.requireActual('./auth').default;
    
    jest.mock('./auth');
    jest.mock('./ws');
    jest.mock('./other-stuff');
    
    const makeSpyMiddleware = () => {
      const dispatch = jest.fn();
      return {
        dispatch,
        middleware: store => next => action => {
          dispatch(action);
          return next(action);
        }
      };
    };
    
    describe('connect', () => {
      let store;
      let spy;
    
      beforeEach(() => {
        jest.clearAllMocks();
        spy = makeSpyMiddleware();
    
        store = createStore(authReducer, {}, applyMiddleware(spy.middleware, thunk));
    
        auth.mockImplementation((login, password) => () => {
          if (login === 'user' && password == 'password') return Promise.resolve();
          return Promise.reject();
        });
      });
    
      test('happy path', () => {
        getToken.mockImplementation(() => 'generated token');
        otherStuff.mockImplementation(token => ({ type: 'mocked/other-stuff', token }));
    
        return store.dispatch(connect('user', 'password')).then(() => {
          expect(wsApi.connect).toHaveBeenCalledWith('generated token');
          expect(spy.dispatch).toHaveBeenCalledWith({ type: 'mocked/other-stuff', token: 'generated token'});
        });
      });
    
      test('auth failed', () => {
        return store.dispatch(connect('user', 'wrong-password')).catch(() => {
          expect(wsApi.connect).not.toHaveBeenCalled();
        });
      });
    });
    

    如果您需要对给定的代码段发表任何评论,请随时提出。