测试 Sagas
测试 Sagas 主要有两种方式:逐步测试 saga 生成器函数或运行完整的 saga 并断言副作用。
测试 Saga 生成器函数
假设我们有以下操作:
const CHOOSE_COLOR = 'CHOOSE_COLOR'
const CHANGE_UI = 'CHANGE_UI'
const chooseColor = color => ({
type: CHOOSE_COLOR,
payload: {
color,
},
})
const changeUI = color => ({
type: CHANGE_UI,
payload: {
color,
},
})
我们想要测试这个 saga:
function* changeColorSaga() {
const action = yield take(CHOOSE_COLOR)
yield put(changeUI(action.payload.color))
}
由于 Sagas 总是产生一个 Effect,这些 effects 有基本的工厂函数(例如 put,take 等),测试可以检查产生的 effect 并将其与预期的 effect 进行比较。要从 saga 获取第一个产生的值,调用其 next().value
:
const gen = changeColorSaga()
assert.deepEqual(gen.next().value, take(CHOOSE_COLOR), '它应该等待用户选择颜色')
然后必须返回一个值以赋值给 action
常量,该常量用作 put
effect 的参数:
const color = 'red'
assert.deepEqual(
gen.next(chooseColor(color)).value,
put(changeUI(color)),
'它应该派发一个操作来改变 UI',
)
由于没有更多的 yield
,所以下次调用 next()
时,生成器将完成:
assert.deepEqual(gen.next().done, true, '它应该完成')
分支 Saga
有时你的 saga 会有不同的结果。为了测试不同的分支而不重复所有导致它的步骤,你可以使用实用函数 cloneableGenerator
这次我们添加两个新的操作,CHOOSE_NUMBER
和 DO_STUFF
,以及相关的操作创建器:
const CHOOSE_NUMBER = 'CHOOSE_NUMBER'
const DO_STUFF = 'DO_STUFF'
const chooseNumber = number => ({
type: CHOOSE_NUMBER,
payload: {
number,
},
})
const doStuff = () => ({
type: DO_STUFF,
})
现在要测试的 saga 将在等待 CHOOSE_NUMBER
操作并放置 changeUI('red')
或 changeUI('blue')
之前,放置两个 DO_STUFF
操作,具体取决于数字是偶数还是奇数。
function* doStuffThenChangeColor() {
yield put(doStuff())
yield put(doStuff())
const action = yield take(CHOOSE_NUMBER)
if (action.payload.number % 2 === 0) {
yield put(changeUI('red'))
} else {
yield put(changeUI('blue'))
}
}
测试如下:
import { put, take } from 'redux-saga/effects'
import { cloneableGenerator } from '@redux-saga/testing-utils'
test('doStuffThenChangeColor', assert => {
const gen = cloneableGenerator(doStuffThenChangeColor)()
gen.next() // DO_STUFF
gen.next() // DO_STUFF
gen.next() // CHOOSE_NUMBER
assert.test('用户选择了一个偶数', a => {
// 在发送数据之前克隆生成器
const clone = gen.clone()
a.deepEqual(clone.next(chooseNumber(2)).value, put(changeUI('red')), '应该将颜色改为红色')
a.equal(clone.next().done, true, '它应该完成')
a.end()
})
assert.test('用户选择了一个奇数', a => {
const clone = gen.clone()
a.deepEqual(clone.next(chooseNumber(3)).value, put(changeUI('blue')), '应该将颜色改为蓝色')
a.equal(clone.next().done, true, '它应该完成')
a.end()
})
})
另请参见:任务取消 用于测试 fork effects
测试完整的 Saga
尽管测试 saga 的每一步可能很有用,但实际上这会使测试变得脆弱。相反,可能更希望运行整个 saga 并断言已发生的预期效果。
假设我们有一个基本的 saga,它调用一个 HTTP API:
function* callApi(url) {
const someValue = yield select(somethingFromState)
try {
const result = yield call(myApi, url, someValue)
yield put(success(result.json()))
return result.status
} catch (e) {
yield put(error(e))
return -1
}
}
我们可以用模拟值运行 saga:
const dispatched = []
const saga = runSaga(
{
dispatch: action => dispatched.push(action),
getState: () => ({ value: 'test' }),
},
callApi,
'http://url',
)
然后可以编写一个测试来断言派发的操作和模拟调用:
import sinon from 'sinon'
import * as api from './api'
test('callApi', async assert => {
const dispatched = []
sinon.stub(api, 'myApi').callsFake(() => ({
json: () => ({
some: 'value',
}),
}))
const url = 'http://url'
const result = await runSaga(
{
dispatch: action => dispatched.push(action),
getState: () => ({ state: 'test' }),
},
callApi,
url,
).toPromise()
assert.true(myApi.calledWith(url, somethingFromState({ state: 'test' })))
assert.deepEqual(dispatched, [success({ some: 'value' })])
})
另请参见:仓库示例:
https://github.com/redux-saga/redux-saga/blob/main/examples/counter/test/sagas.js
https://github.com/redux-saga/redux-saga/blob/main/examples/shopping-cart/test/sagas.js
测试库
虽然上述两种测试方法都可以原生编写,但存在几个库可以使两种方法更容易。此外,有些库可以用第三种方式测试 sagas:记录特定的副作用(但不是全部)。
Sam Hogarth的 (@sh1989) 文章 很好地总结了不同的选项。
对于逐步测试每个生成器的产出,有 redux-saga-test
和 redux-saga-testing
。redux-saga-test-engine
用于记录和测试特定的副作用。对于集成测试,有 redux-saga-tester
。而 redux-saga-test-plan
实际上可以覆盖所有三个基础。