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.