测试 Sagas

有两个主要的测试 Sagas 的方式:一步一步测试 saga generator function,或者执行整个 saga 并断言 side effects。

测试 Saga Generator Function

假设我们有以下的 action:

  1. const CHOOSE_COLOR = 'CHOOSE_COLOR';
  2. const CHANGE_UI = 'CHANGE_UI';
  3. const chooseColor = (color) => ({
  4. type: CHOOSE_COLOR,
  5. payload: {
  6. color,
  7. },
  8. });
  9. const changeUI = (color) => ({
  10. type: CHANGE_UI,
  11. payload: {
  12. color,
  13. },
  14. });

我们想要测试 saga:

  1. function* changeColorSaga() {
  2. const action = yield take(CHOOSE_COLOR);
  3. yield put(changeUI(action.payload.color));
  4. }

由于 Sagas 总是会 yield 一个 Effect,并且这些 effects 有简单的 factory function(例如 put, take 等等),测试将检查被 yield 的 effect,并拿它和期望返回的 effect 进行比对。 调用 next().value 来获取 saga 的第一个被 yield 的值:

  1. const gen = changeColorSaga();
  2. assert.deepEqual(
  3. gen.next().value,
  4. take(CHOOSE_COLOR),
  5. 'it should wait for a user to choose a color'
  6. );

应该返回一个值,赋给 action 常量,它将被用于 put effect 的参数:

  1. const color = 'red';
  2. assert.deepEqual(
  3. gen.next(chooseColor(color)).value,
  4. put(changeUI(color)),
  5. 'it should dispatch an action to change the ui'
  6. );

由于没有更多其他的 yield,下一次调用 next() 时,generator 将会完成:

  1. assert.deepEqual(
  2. gen.next().done,
  3. true,
  4. 'it should be done'
  5. );

Branching Saga

有时候你的 saga 可能会有不同的结果。为了测试不同的 branch 而不重复所有流程,你可以使用 cloneableGenerator utility function

这时候我们新增两个 action,CHOOSE_NUMBERDO_STUFF,使用一个 action creator 来创建:

  1. const CHOOSE_NUMBER = 'CHOOSE_NUMBER';
  2. const DO_STUFF = 'DO_STUFF';
  3. const chooseNumber = (number) => ({
  4. type: CHOOSE_NUMBER,
  5. payload: {
  6. number,
  7. },
  8. });
  9. const doStuff = () => ({
  10. type: DO_STUFF,
  11. });

现在,在 CHOOSE_NUMBER action 之前,测试中的 saga 将会 put 两个 DO_STUFF action,然后根据数字是奇数还是偶数 put changeUI('red')changeUI('blue')

  1. function* doStuffThenChangeColor() {
  2. yield put(doStuff());
  3. yield put(doStuff());
  4. const action = yield take(CHOOSE_NUMBER);
  5. if (action.payload.number % 2 === 0) {
  6. yield put(changeUI('red'));
  7. } else {
  8. yield put(changeUI('blue'));
  9. }
  10. }

测试如下:

  1. import { put, take } from 'redux-saga/effects';
  2. import { cloneableGenerator } from 'redux-saga/utils';
  3. test('doStuffThenChangeColor', assert => {
  4. const gen = cloneableGenerator(doStuffThenChangeColor)();
  5. gen.next(); // DO_STUFF
  6. gen.next(); // DO_STUFF
  7. gen.next(); // CHOOSE_NUMBER
  8. assert.test('user choose an even number', a => {
  9. // cloning the generator before sending data
  10. const clone = gen.clone();
  11. a.deepEqual(
  12. clone.next(chooseNumber(2)).value,
  13. put(changeUI('red')),
  14. 'should change the color to red'
  15. );
  16. a.equal(
  17. clone.next().done,
  18. true,
  19. 'it should be done'
  20. );
  21. a.end();
  22. });
  23. assert.test('user choose an odd number', a => {
  24. const clone = gen.clone();
  25. a.deepEqual(
  26. clone.next(chooseNumber(3)).value,
  27. put(changeUI('blue')),
  28. 'should change the color to blue'
  29. );
  30. a.equal(
  31. clone.next().done,
  32. true,
  33. 'it should be done'
  34. );
  35. a.end();
  36. });
  37. });

参与 Task cancellation 来测试 fork effects

测试完整的 Saga

虽然测试 saga 的每一步可能是有用的,实际上这使得 brittle tests 成为可能。不过,运行整个 saga 并断言预期的 effects 已经产生可能会更好。

假设我们有一个简单的 saga,它将调用一个 HTTP API:

  1. function* callApi(url) {
  2. const someValue = yield select(somethingFromState);
  3. try {
  4. const result = yield call(myApi, url, someValue);
  5. yield put(success(result.json()));
  6. return result.status;
  7. } catch (e) {
  8. yield put(error(e));
  9. return -1;
  10. }
  11. }

我们可以使用 mock 的数据来运行这个 saga:

  1. const dispatched = [];
  2. const saga = runSaga({
  3. dispatch: (action) => dispatched.push(action),
  4. getState: () => ({ value: 'test' }),
  5. }, callApi, 'http://url');

然后可以写一个测试来断言被 dispatch 的 action 和模拟的调用:

  1. import sinon from 'sinon';
  2. import * as api from './api';
  3. test('callApi', async (assert) => {
  4. const dispatched = [];
  5. sinon.stub(api, 'myApi').callsFake(() => ({
  6. json: () => ({
  7. some: 'value'
  8. })
  9. }));
  10. const url = 'http://url';
  11. const result = await runSaga({
  12. dispatch: (action) => dispatched.push(action),
  13. getState: () => ({ state: 'test' }),
  14. }, callApi, url).done;
  15. assert.true(myApi.calledWith(url, somethingFromState({ state: 'test' })));
  16. assert.deepEqual(dispatched, [success({ some: 'value' })]);
  17. });

也可以查看仓库示例:

https://github.com/redux-saga/redux-saga/blob/master/examples/counter/test/sagas.js

https://github.com/redux-saga/redux-saga/blob/master/examples/shopping-cart/test/sagas.js