redux-saga
框架提供了许多有趣的副作用处理模式,但是,像真正的血腥企业开发人员一样,我们需要用测试覆盖整个代码。 让我们弄清楚如何测试我们的sagas。

以最简单的答题器为例。 数据流和应用程序的含义如下:
- 用户拨一个按钮。
- 请求被发送到服务器,通知用户已按下按钮。
- 服务器返回点击次数。
- 该状态记录点击次数。
- 用户界面已更新,用户看到点击次数增加了。
- ...
- 利润。
在我们的工作中,我们使用Typescript,因此所有示例均使用该语言。
您可能已经猜到了,我们将使用
redux-saga
实现所有这些。 这是整个sagas文件的代码:
export function* processClick() { const result = yield call(ServerApi.SendClick) yield put(Actions.clickSuccess(result)) } export function* watchClick() { yield takeEvery(ActionTypes.CLICK, processClick) }
在这个简单的示例中,我们声明了直接处理动作的saga
watchClick
和创建了处理
action'
的循环的saga
watchClick
。
发电机
所以我们有最简单的传奇。 它向服务器发送一个请求
( call)
,接收结果并将其传递给reducer
( put)
。 我们需要以某种方式测试传奇故事是否正在准确传输它从服务器接收到的内容。 让我们开始吧。
为了进行测试,我们需要锁定服务器调用,并以某种方式检查来自服务器的确切信息是否进入了reducer。
由于sagas是生成器函数,因此生成器原型中的
next()
方法将是最明显的测试方法。 使用这种方法时,我们有机会既从生成器接收下一个值,又将值传输到生成器。 因此,我们开箱即用了打来电话的机会。 但是,一切都那么美好吗? 这是我在裸机上编写的测试:
it('should increment click counter (behaviour test)', () => { const saga = processClick() expect(saga.next().value).toEqual(call(ServerApi.SendClick)) expect(saga.next(10).value).toEqual(put(Actions.clickSuccess(10))) })
测试很简洁,但是它测试了什么? 实际上,它只是重复了saga方法的代码,也就是说,随着saga的任何更改,都必须更改测试。
这样的测试对开发没有帮助。
Redux传奇测试计划
遇到此问题后,我们决定对其进行谷歌搜索,突然意识到我们不是唯一的人,而且距离第一个人还很遥远。 开发人员可以直接
在 redux-saga
的文档中查看专门为满足测试爱好者而创建的几个库。
从提议的列表中,我们采用了
redux-saga-test-plan
库。 这是我用它编写的测试的第一个版本的代码:
it('should increment click counter (behaviour test with test-plan)', () => { return expectSaga(processClick) .provide([ call(ServerApi.SendClick), 2] ]) .dispatch(Actions.click()) .call(ServerApi.SendClick) .put(Actions.clickSuccess(2)) .run() })
redux-saga-test-plan
的测试构造函数是
expectSaga
函数,该函数返回描述测试的接口。 测试传奇(第一个清单中的
processClick
)传递给函数本身。
使用
provide
方法,可以阻止服务器调用或其他依赖项。 将一个
StaticProvider'
数组
StaticProvider'
,该数组描述应返回的方法。
在
Act
块中,我们只有一个方法
dispatch
。 动作将传递给它,传奇将对此做出响应。
assert
块由
call put
方法组成,它们检查传奇故事工作期间是否引起了相应的影响。
一切都以
run()
方法结束。 此方法直接运行测试。
这种方法的优点:
- 它检查方法是否被调用,而不是调用顺序。
- moki清楚地描述了哪个功能弄湿了什么返回了。
但是,有一些工作要做:
- 还有更多代码;
- 该测试难以阅读;
- 这是对行为的测试,这意味着它仍与传奇的实现相关。
最后两招
条件测试
首先,我们修复最后一个问题:我们从行为测试中进行状态测试。
test-plan
允许您设置初始
state
并通过
reducer
的事实,它应该响应传奇产生的
put
效果,这将帮助我们解决这一问题。 看起来像这样:
it('should increment click counter (state test with test-plan)', () => { const initialState = { clickCount: 11, return expectSaga(processClick) .provide([ call(ServerApi.SendClick), 14] ]) .withReducer(rootReducer, initialState) .dispatch(Actions.click()) .run() .then(result => expect(result.storeState.clickCount).toBe(14)) })
在此测试中,我们不再验证是否触发了任何效果。 执行后我们检查最终状态,这很好。
我们设法摆脱了传奇的实现,现在让我们尝试使测试更易于理解。 如果将
then()
替换为
async/await
这很容易:
it('should increment click counter (state test with test-plan async-way)', async () => { const initialState = { clickCount: 11, } const saga = expectSaga(processClick) .provide([ call(ServerApi.SendClick), 14] ]) .withReducer(rootReducer, initialState) const result = await saga.dispatch(Actions.click()).run() expect(result.storeState.clickCount).toBe(14) })
整合测试
但是,如果我们还进行了反向点击操作(我们将其称为unclick),现在我们的sag文件如下所示:
export function* processClick() { const result = yield call(ServerApi.SendClick) yield put(Actions.clickSuccess(result)) } export function* processUnclick() { const result = yield call(ServerApi.SendUnclick) yield put(Actions.clickSuccess(result)) } function* watchClick() { yield takeEvery(ActionTypes.CLICK, processClick) } function* watchUnclick() { yield takeEvery(ActionTypes.UNCLICK, processUnclick) } export default function* mainSaga() { yield all([watchClick(), watchUnclick()]) }
假设我们需要测试,当在状态中调用click和unclick动作时,最后一次访问服务器的结果将写入state。 这样的测试也可以通过
redux-saga-test-plan
轻松完成:
it('should change click counter (integration test)', async () => { const initialState = { clickCount: 11, } const saga = expectSaga(mainSaga) .provide([ call(ServerApi.SendClick), 14], call(ServerApi.SendUnclick), 18] ]) .withReducer(rootReducer, initialState) const result = await saga .dispatch(Actions.click()) .dispatch(Actions.unclick()) .run() expect(result.storeState.clickCount).toBe(18) })
请注意,现在我们正在测试
mainSaga
,而不是单个
mainSaga
处理程序。
但是,如果我们按原样运行此测试,则会得到Vorning:

这是由于
takeEvery
效果-这是一个消息处理循环,它将在我们的应用程序打开时起作用。 因此,在没有外部帮助的情况下调用
takeEvery
的测试
takeEvery
无法完成工作,并且
redux-saga-test-plan
在测试开始后250毫秒后会强制终止这些影响。 可以通过调用ExpectSaga.DEFAULT_TIMEOUT = 50来更改此超时。
如果您不想收到这样的折衷,那么对于每个具有复杂效果的测试,只需使用silentRun()
方法而不是run()
方法即可。
陷阱
没有陷阱的地方...在撰写本文时,redux-saga的最新版本为1.0.2。 同时,
redux-saga-test-plan
只能在JS上使用。
如果需要TypeScript,则必须从beta通道安装版本:
npm install redux-saga-test-plan@beta
并从构建中关闭测试。 为此,在tsconfig.json文件中,您需要在“排除”字段中指定路径“ ./src/**/*.spec.ts”。
尽管如此,我们还是认为
redux-saga-test-plan
是测试
redux-saga
的最佳库。 如果您的项目中有
redux-saga
,也许对您来说是个不错的选择。
GitHub上示例的源代码。