The best way to test Redux Sagas
Tags: react, redux, javascript
Published: 2018-12-10 10:00:16

---
title: The best way to test Redux Sagas
published: true
description: How we made our redux-saga tests more flexible and more fun to write
tags: react, redux, javascript
---

tl;dr: to test a saga, it's **way, way** better to run it as a whole (using `runSaga()`) than to do it step-by-step (using `gen.next()`)

In my team, we're currently using `redux-saga` to handle asynchronous calls in our React/Redux application. These sagas can call APIs and dispatch actions using [ES6 generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*). Below is a contrived example, in which we load a profile. After the `yield` statements, you can see 3 side effects that tend to show up in our team's sagas:
* `select` "instructs the middleware to invoke the provided selector" on the store
* `put` "instructs the middleware to dispatch an action" to the store
* `call` instructs the middleware to call the given function

You can find full descriptions in the [API reference](https://redux-saga.js.org/docs/api/).

All the code snippets in this blog can be found in [this example repository](https://github.com/philherbert/saga-testing-examples).

```javascript
import {call, put, select} from 'redux-saga/effects';
import {isAuthenticated} from './selectors';
import {loadProfileFailure, loadProfileSuccess} from './actionCreators';
import {getProfile} from './api';

export function* loadProfileSaga(action) {
  // use a selector to determine if the user is authenticated
  const authenticated = yield select(isAuthenticated);
  if (authenticated) {
    // call the API and dispatch a success action with the profile
    const profile = yield call(getProfile, action.profileId);
    yield put(loadProfileSuccess(profile));
  } else {
    // dispatch a failure action
    yield put(loadProfileFailure());
  }
}

```

# Testing sagas step-by-step is rubbish

To test sagas, our approach so far has been to call the generator function to get the iterator object, and then to manually call `.next()` to bump through the `yield` statements, asserting on the value of each `yield` as we go.

To test that the saga dispatches a failure action if the user is not authenticated, we can assert that the first `gen.next()` - i.e. the first `yield` - calls the selector.

Then, to pretend that the selector returned false, we need to **pass a pretend return value from the selector into the following `gen.next()`**. That's why we have to call `gen.next(false).value` in the test below. Without an intimate understanding of generators, this syntax is alien and opaque.

```javascript
it('should fail if not authenticated', () => {
  const action = {profileId: 1};
  const gen = loadProfileSaga(action);

  expect(gen.next().value).toEqual(select(isAuthenticated));
  expect(gen.next(false).value).toEqual(put(loadProfileFailure()));
  expect(gen.next().done).toBeTruthy();
});
```

Next, let's test the case where the user is authenticated. It's not really necessary to assert that the first `yield` is a `select()`, since we did that in the previous test. To avoid the duplicate assertion, we can write `gen.next()` outside of an assertion to just skip over it. However, when reading the test in isolation, this **`gen.next()` is just a magic incantation**, whose purpose is not clear. Like in the previous test, we can call `gen.next(true).value` to pretend that the selector has returned `true`.

Then, we can test that the following `yield` is the API call, **pass some pretend return value of `getProfile()` into the following `gen.next()`** and assert that the success action is dispatched with that same return value. 

```javascript
it('should get profile from API and call success action', () => {
  const action = {profileId: 1};
  const gen = loadProfileSaga(action);

  const someProfile = {name: 'Guy Incognito'};

  gen.next();
  expect(gen.next(true).value).toEqual(call(getProfile, 1));
  expect(gen.next(someProfile).value).toEqual(put(loadProfileSuccess(someProfile)));
  expect(gen.next().done).toBeTruthy();
});
```

## Why is step-by-step testing bad?

### Unintuitive test structure

Outside of saga-land, 99% of tests that we write roughly follow an Arrange-Act-Assert structure. For our example, that would be something like this:

```javascript
it('should fail if not authenticated', () => {
  given that the user is not authenticated

  when we load the profile

  then loading the profile fails
});
```

For sagas, the conditions of our tests could be the results of side effects like `yield call` or `yield select`. The results of these effects are passed as arguments into the `gen.next()` call that immediately follows, **which is often itself inside an assert**. This is why the first example test above includes these two lines:

```javascript
                        // this is the call that we want to "stub"
                        //                  ↓
expect(gen.next().value).toEqual(select(isAuthenticated));
expect(gen.next(false).value).toEqual(put(loadProfileFailure()));
    //            ↑
    //  this is the return value (!)
```

So, rather than Arrange-Act-Assert, the example saga tests above are more like this:

```javascript
it('should fail if not authenticated', () => {
    create the iterator
    for each step of the iterator:
      assert that, given the previous step returns some_value, 
      the next step is a call to someFunction()
});
```

### Difficult to test negatives

For the example saga, it would be reasonable to test that we don't call the API if the user is not authenticated. But if we're testing each `yield` step-by-step, and we don't want to make assumptions about the internal structure of the saga, the only thorough way to do this is to run through every `yield` and assert that none of them call the API.

```javascript
expect(gen.next().value).not.toEqual(call(getProfile));
expect(gen.next().value).not.toEqual(call(getProfile));
...
expect(gen.next().done).toBeTruthy();
```

We want to assert that `getProfile()` is never called, but instead we have to check that every `yield` is not a call to `getProfile()`.

### Coupling between test and implementation

Our tests closely replicate our production code. We have to bump through the `yield` statements of the saga, asserting that they yield the right things, and as a byproduct, asserting that they are called in some fixed order.

The tests are brittle, and refactoring or extending the sagas is incredibly difficult.

If we reorder the side effects, we need to fix all of our `expect(gen.next(foo).value)` assertions, to make sure we're passing the right return value into the right `yield` statement.

If we dispatch an additional action with a new `yield put()` near the top of a saga, the tests will all have to have an additional `gen.next()` added in somewhere, to skip over that `yield`, and move the assertions "one yield down".

I have frequently stared at a failing test, repeatedly trying to insert `gen.next()` in various places, blindly poking until it passes.

# A better way is to run the whole saga

What if we could set up the conditions of our test, instruct the saga to run through everything and finish its business, and then check that the expected side effects have happened? That's roughly how we test every other bit of code in our application, and there's no reason we can't do that for sagas too.

The golden ticket here is our utility function `recordSaga()`, which uses `redux-saga`'s `runSaga()` to start a given saga outside of the middleware, with a given action as a parameter. The options object is used to define the behaviour of the saga's side effects. Here, we're only using `dispatch`, which fulfils `put` effects. The given function adds the dispatched actions to a list, which is returned after the saga is finished executing.

```javascript
import {runSaga} from 'redux-saga';

export async function recordSaga(saga, initialAction) {
  const dispatched = [];

  await runSaga(
    {
      dispatch: (action) => dispatched.push(action)
    },
    saga,
    initialAction
  ).done;

  return dispatched;
}
```

With this, we can mock some functions to set up the test's conditions, run the saga as a whole, and then assert on the list of actions dispatched or functions called to check its side effects. Amazing! Consistent! Familiar!

*Note: it's possible to pass a store into `runSaga()` that selectors would be run against, as in [the example in the documentation](https://redux-saga.js.org/docs/advanced/Testing.html). However, instead of building a fake store with the correct structure, we've found it easier to stub out the selectors.*

Here's the necessary set up, which can go in a `describe()` block. We're using `jest` to stub the functions that the saga imports.

```javascript
api.getProfile = jest.fn();
selectors.isAuthenticated = jest.fn();

beforeEach(() => {
  jest.resetAllMocks();
});
```

For our first test, we can set up the conditions of our test using the stubbed selector, run through the saga, and then assert on the actions it dispatched. We can also assert that the API call was never made!

```javascript
it('should fail if not authenticated', async () => {
  selectors.isAuthenticated.mockImplementation(() => false);

  const initialAction = {profileId: 1};
  const dispatched = await recordSaga(
    loadProfileSaga,
    initialAction
  );

  expect(dispatched).toContainEqual(loadProfileFailure());
  expect(api.getProfile).not.toHaveBeenCalled();
});
```

In our second test, we can mock the implementation of the API function to return a profile, and then later, assert that the `loadProfileSuccess()` action was dispatched, with the correct profile.

```javascript
it('should get profile from API and call success action if authenticated', async () => {
  const someProfile = {name: 'Guy Incognito'};
  api.getProfile.mockImplementation(() => someProfile);
  selectors.isAuthenticated.mockImplementation(() => true);

  const initialAction = {profileId: 1};
  const dispatched = await recordSaga(
    loadProfileSaga,
    initialAction
  );

  expect(api.getProfile).toHaveBeenCalledWith(1);
  expect(dispatched).toContainEqual(loadProfileSuccess(someProfile));
});
```

## Why is it better to test as a whole?

* **Familiar test structure**, matching the Arrange-Act-Assert layout of every other test in our application.
* **Easier to test negatives**, because the saga will actually call functions, so we have the full power of mocks at our disposal.
* **Decoupled from the implementation**, since we're no longer testing the number or order of `yield` statements. I think this is absolutely the main reason why this approach is preferable. **Instead of testing the internal details of the code, we're testing its public API** - that is, its side effects.

The two approaches to testing sagas are mentioned in the [`redux-saga` documentation](https://redux-saga.js.org/docs/advanced/Testing.html), but I'm surprised the step-by-step method is even discussed. Testing a saga as a whole is conceptually familiar, and considerably less brittle.

---

Heavily inspired by [this github issue](https://github.com/redux-saga/redux-saga/issues/518).