Redux Saga Test Engine
redux-saga-test-engine is a testing utility designed to simplify the process of testing Redux Saga generator functions. It provides a structured API for collecting specific Redux Saga effects (like `PUT` and `CALL`) and mapping yielded effects to predefined return values, eliminating the need for manual iteration over generator steps. The current stable version is 3.0.0. While no explicit release cadence is stated, major versions introduce breaking changes to the top-level API, with minor versions providing feature enhancements and bug fixes. It differentiates itself from alternatives like `redux-saga-test` and `redux-saga-test-plan` by offering a distinct approach to effect collection and environment stubbing, aiming for a more direct and less verbose testing experience, particularly when focusing on the effects a saga produces rather than its step-by-step execution.
Common errors
-
TypeError: response.json is not a function
cause A `call` effect was stubbed with a value that does not have the `.json()` method, but the saga attempted to call it.fixEnsure the stub for the `call` effect returns an object that accurately mimics the expected API response, including any methods like `json()` that the saga will invoke. Example: `[call(API.fetch), { json: () => ({}) }]` or using `stub()` with a generator that yields such an object. -
Test timeout: Async callback was not invoked within the timeout period.
cause A `select` or `call` effect within the tested saga did not have a corresponding entry in the `envMapping`, preventing the test engine from progressing the saga.fixInspect the saga under test and the `envMapping` to confirm that every `select` and `call` effect is explicitly mapped to a return value. Ensure the effect object passed as the key in the mapping exactly matches the effect yielded by the saga.
Warnings
- breaking The top-level API underwent significant changes in version 2.0.0. Existing tests written for 1.x versions will require updates, particularly regarding how effects are collected and how the testing engine is initialized. The `createSagaTestEngine` function and explicit effect collection array (`['PUT', 'CALL']`) are part of the new API.
- gotcha When stubbing `call` effects that return objects with methods (e.g., an API response object that you expect to call `.json()` on), ensure your stub provides an object with the expected method. Otherwise, your saga will throw an error like 'response.json is not a function'.
- gotcha All non-`PUT` effects yielded by the saga (e.g., `select`, `call`) must have a corresponding mapping in the `envMapping` argument. If an effect is yielded that isn't mapped, `redux-saga-test-engine` will not know what value to return to the saga, potentially causing the saga to hang or the test to fail unexpectedly.
Install
-
npm install redux-saga-test-engine -
yarn add redux-saga-test-engine -
pnpm add redux-saga-test-engine
Imports
- createSagaTestEngine
const { createSagaTestEngine } = require('redux-saga-test-engine')import { createSagaTestEngine } from 'redux-saga-test-engine' - collectPuts
const collectPuts = require('redux-saga-test-engine').collectPutsimport { collectPuts } from 'redux-saga-test-engine' - stub
const stub = require('redux-saga-test-engine').stubimport { stub } from 'redux-saga-test-engine' - throwError
const throwError = require('redux-saga-test-engine').throwErrorimport { throwError } from 'redux-saga-test-engine'
Quickstart
import { createSagaTestEngine } from 'redux-saga-test-engine';
import { select, call, put } from 'redux-saga/effects';
// Mock API and selectors for the example
const API = {
doWeLovePuppies: () => ({ answer: 'Of course we do!' })
};
const getPuppy = () => ({ barks: true, cute: 'Definitely' });
const petPuppy = (puppy) => ({ type: 'PET_PUPPY', payload: puppy });
const hugPuppy = (puppy) => ({ type: 'HUG_PUPPY', payload: puppy });
// The saga to be tested
function* sagaToTest(action) {
const puppyState = yield select(getPuppy);
const apiResponse = yield call(API.doWeLovePuppies);
if (puppyState.cute && apiResponse.answer) {
yield put(petPuppy(puppyState));
yield put(hugPuppy(puppyState));
}
}
// Choose which effect types you want to collect from the saga.
const collectEffects = createSagaTestEngine(['PUT', 'CALL']);
// Define environment mappings and initial action
const initialAction = { type: 'START_SAGA' };
const envMapping = [
[select(getPuppy), { barks: true, cute: 'Definitely' }],
[call(API.doWeLovePuppies), { answer: 'Of course we do!' }]
];
const actualEffects = collectEffects(
sagaToTest,
envMapping,
initialAction
);
console.log(JSON.stringify(actualEffects, null, 2));
/*
Expected output (simplified):
[
{ "@@redux-saga/IO": true, "call": { "args": [], "fn": {} } }, // call(API.doWeLovePuppies)
{ "@@redux-saga/IO": true, "put": { "action": { "type": "PET_PUPPY", "payload": { "barks": true, "cute": "Definitely" } } } },
{ "@@redux-saga/IO": true, "put": { "action": { "type": "HUG_PUPPY", "payload": { "barks": true, "cute": "Definitely" } } } }
]
*/