简答:嘲笑它。 =)
长答案:我个人更喜欢在测试中尽可能多地使用真实代码(即没有测试替身)。但有时它只是不值得,你不得不回退到嘲笑。
在您所描述的情况下,您可能希望/需要在测试中检查几件事:
- 某些子 thunk 实际上是从您的被测 thunk 中分派的。
- 正在测试的 thunk 可以正确处理它分派的子 thunk 的结果。
- 正在测试的 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: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();
});
});
});
如果您在给定的 sn-ps 上需要任何 cmets,请随时询问。