【问题标题】:Testing complex redux thunks with conditional logic and getState() usage使用条件逻辑和 getState() 测试复杂的 redux thunk
【发布时间】:2018-01-16 02:30:32
【问题描述】:

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

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

我对整个测试都是新手,互联网上的所有示例都是 TODO 列表或其他可笑的简单 CRUD 应用程序,这无济于事。您如何在复杂的应用程序中实际进行 redux 测试,这些应用程序使用大量条件逻辑,以及依赖于多个状态节点的动作创建者?

【问题讨论】:

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


【解决方案1】:

简答:嘲笑它。 =)

长答案:我个人更喜欢在测试中尽可能多地使用真实代码(即没有测试替身)。但有时它只是不值得,你不得不回退到嘲笑。

在您所描述的情况下,您可能希望/需要在测试中检查几件事:

  1. 某些子 thunk 实际上是从您的被测 thunk 中分派的。
  2. 正在测试的 thunk 可以正确处理它分派的子 thunk 的结果。
  3. 正在测试的 thunk 正确处理更新的状态,该状态由调度的子 thunk 更新。

根据您要测试的上述内容的组合,可以使用不同的策略。例如,如果您的被测 thunk 依赖于 sub-thunk 的结果,则无需检查是否调度了 sub-thunk:只需模拟 sub-thunk 以便它返回特定数据,这将影响特定数据中被测 thunk 的行为,明智的方式(见authmock 以了解下面sn-p 中的详细信息)。

让我们考虑以下示例来说明可能的模拟策略。想象一下,您必须实现授权功能。假设您的服务器通过 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();
    });
  });
});

如果您在给定的 sn-ps 上需要任何 cmets,请随时询问。

【讨论】:

    猜你喜欢
    • 2018-06-03
    • 2021-10-04
    • 2021-08-18
    • 2018-09-27
    • 2018-01-31
    • 1970-01-01
    • 2016-04-12
    • 2020-02-03
    • 1970-01-01
    相关资源
    最近更新 更多