我的应用程序中的所有逻辑都存在于动作创建者(thunk)中。大多数动作创作者'逻辑并不复杂,它由条件表达式组成,其条件是来自商店的值:如果商店中存在此值,则调度这些操作创建者,否则调度此操作。还有一些"聚合器",它们是动作创建者,通常根据某些状态值的存在来调度其他几个动作创建者;和api包装器,它有条件地用来自状态的参数调用api抽象thunk,然后处理响应。
点是,大多数人使用getState函数来获取他们自己需要的所有东西,而不是将其作为参数接收。现在,这种方法对我很有用,并且很容易使用,但是我很难对它进行测试。到目前为止,我按照这个建议编写了所有测试:https://github.com/reactjs/redux/issues/2179。基本上,在开始时我使用其他一些操作设置所需的状态,模拟获取调用,然后调度我打算测试的thunk,然后使用各种选择器检查状态。它在一次测试中同时测试多个动作,缩减器和选择器。我喜欢我的测试完全验证特定用例的事实,但我不确定这实际上是否是一个好习惯。我的主要问题是一些thunk是不可测试的,因为他们派遣了5个其他的动作创建者,我很困惑如何至少验证他们被调用,除了检查状态是否已经改变,这反过来使Promise链巨大的,并在多个测试中一遍又一遍地测试相同的功能。
我对这整个测试事项都很陌生,互联网上的所有示例都是TODO列表或其他非常简单的CRUD应用程序,这些都没有用。你如何在复杂的应用程序中进行redux测试,这些应用程序使用大量条件逻辑,而动作创建器依赖于多个状态节点?
答案 0 :(得分:1)
简短回答:嘲笑。 =)
答案很长:我个人更喜欢在测试中使用尽可能多的真实代码(即没有测试双打)。但有时候它并不值得,你必须回归嘲笑。
在您描述的情况下,您可能需要/需要检查几项测试内容:
根据您想要测试的上述内容的组合,可以使用不同的策略。例如,不需要检查是否调度了子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:auth
和otherStuff
。 connect
高度依赖于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();
});
});
});
如果您需要对给定的代码段发表任何评论,请随时提出。