There are two main ways to test Sagas: testing the saga generator function step-by-step or running the full saga and asserting the side effects.
Suppose we have the following actions:
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,
},
});
We want to test the saga:
function* changeColorSaga() {
const action = yield take(CHOOSE_COLOR);
yield put(changeUI(action.payload.color));
}
Since Sagas always yield an Effect, and these effects have simple factory functions (e.g. put, take etc.) a test may
inspect the yielded effect and compare it to an expected effect. To get the first yielded value from a saga,
call its next().value
:
const gen = changeColorSaga();
assert.deepEqual(
gen.next().value,
take(CHOOSE_COLOR),
'it should wait for a user to choose a color'
);
A value must then be returned to assign to the action
constant, which is used for the argument to the put
effect:
const color = 'red';
assert.deepEqual(
gen.next(chooseColor(color)).value,
put(changeUI(color)),
'it should dispatch an action to change the ui'
);
Since there are no more yield
s, then next time next()
is called, the generator will be done:
assert.deepEqual(
gen.next().done,
true,
'it should be done'
);
Sometimes your saga will have different outcomes. To test the different branches without repeating all the steps that lead to it you can use the utility function cloneableGenerator
This time we add two new actions, CHOOSE_NUMBER
and DO_STUFF
, with a related action creators:
const CHOOSE_NUMBER = 'CHOOSE_NUMBER';
const DO_STUFF = 'DO_STUFF';
const chooseNumber = (number) => ({
type: CHOOSE_NUMBER,
payload: {
number,
},
});
const doStuff = () => ({
type: DO_STUFF,
});
Now the saga under test will put two DO_STUFF
actions before waiting for a CHOOSE_NUMBER
action and then putting
either changeUI('red')
or changeUI('blue')
, depending on whether the number is even or odd.
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'));
}
}
The test is as follows:
import { put, take } from 'redux-saga/effects';
import { cloneableGenerator } from 'redux-saga/utils';
test('doStuffThenChangeColor', assert => {
const gen = cloneableGenerator(doStuffThenChangeColor)();
gen.next(); // DO_STUFF
gen.next(); // DO_STUFF
gen.next(); // CHOOSE_NUMBER
assert.test('user choose an even number', a => {
// cloning the generator before sending data
const clone = gen.clone();
a.deepEqual(
clone.next(chooseNumber(2)).value,
put(changeUI('red')),
'should change the color to red'
);
a.equal(
clone.next().done,
true,
'it should be done'
);
a.end();
});
assert.test('user choose an odd number', a => {
const clone = gen.clone();
a.deepEqual(
clone.next(chooseNumber(3)).value,
put(changeUI('blue')),
'should change the color to blue'
);
a.equal(
clone.next().done,
true,
'it should be done'
);
a.end();
});
});
See also: Task cancellation for testing fork effects
Although it may be useful to test each step of a saga, in practise this makes for brittle tests. Instead, it may be preferable to run the whole saga and assert that the expected effects have occurred.
Suppose we have a simple saga which calls an 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;
}
}
We can run the saga with mocked values:
const dispatched = [];
const saga = runSaga({
dispatch: (action) => dispatched.push(action),
getState: () => ({ value: 'test' }),
}, callApi, 'http://url');
A test could then be written to assert the dispatched actions and mock calls:
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).done;
assert.true(myApi.calledWith(url, somethingFromState({ state: 'test' })));
assert.deepEqual(dispatched, [success({ some: 'value' })]);
});
See also: Repository Examples:
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