How to Test Redux Saga in React
Testing Redux Saga in React applications ensures that complex
asynchronous side effects, like API fetches and state dispatches, run
reliably. This guide explains how to effectively test your sagas using
two main strategies: testing the generator step-by-step using standard
Jest assertions, and performing integration tests using the popular
redux-saga-test-plan library.
Because Redux Sagas are implemented as generator functions, they
yield plain JavaScript objects called “effects” (such as
call or put). This design makes them uniquely
easy to test without actually executing the underlying asynchronous
network requests or database queries.
Step-by-Step Generator Testing
The most basic way to test a saga is by manually iterating through
the generator function using the .next() method. This
approach allows you to assert that the saga yields the correct effect at
each step of its execution.
Consider a saga that fetches user data:
function* fetchUserSaga(action) {
try {
const user = yield call(api.fetchUser, action.payload.userId);
yield put(fetchUserSuccess(user));
} catch (error) {
yield put(fetchUserFailure(error));
}
}To test this generator step-by-step, call .next() and
assert against the yielded value:
it('should fetch user successfully', () => {
const generator = fetchUserSaga({ payload: { userId: 1 } });
// 1. Assert the API call is yielded
expect(generator.next().value).toEqual(
call(api.fetchUser, 1)
);
// 2. Mock the API response by passing it into next(), then assert the success action
const mockUser = { id: 1, name: 'John Doe' };
expect(generator.next(mockUser).value).toEqual(
put(fetchUserSuccess(mockUser))
);
// 3. Ensure the generator is finished
expect(generator.next().done).toBe(true);
});While highly precise, step-by-step testing can be brittle. If you add or reorder a single yield in your saga, the entire test will break even if the end outcome remains the same.
Integration Testing with Redux Saga Test Plan
To avoid the brittleness of step-by-step testing, you can use the
redux-saga-test-plan library. This library runs the saga to
completion and allows you to assert on the final outcome, mocking only
what is necessary.
Here is how you test the same saga using expectSaga:
import { expectSaga } from 'redux-saga-test-plan';
it('should fetch user and dispatch success using expectSaga', () => {
const mockUser = { id: 1, name: 'John Doe' };
return expectSaga(fetchUserSaga, { payload: { userId: 1 } })
.provide([
[call(api.fetchUser, 1), mockUser] // Mock the API call response
])
.put(fetchUserSuccess(mockUser)) // Assert the final dispatch action
.run();
});By using expectSaga, your tests focus on behavior (what
actions are ultimately dispatched or what state changes occur) rather
than implementation details (the exact sequence of yields), making your
test suite much more robust and easier to maintain.