Unit Testing Redux Sagas with Jest

Gaurav KC
4 min readJun 21, 2019

--

Redux saga is a very useful middleware which handles our application side effects like api calls in your react redux data flow. In simpler terms they sit between your actions and reducers and handle all asynchronous logic for you as plain redux action isn’t capable of that.

In a way we are making our actions and reducers dumb with most of the logic handled by saga, so this makes unit testing sagas very important. These sagas are generator functions so let’s understand them first.

Generator functions
These are functions which have a star(asterisk) in them. Unlike normal functions these return a generator object with a series of values. Each iteration has a yield statement and is called with next() method which returns an object like this.

{ value: ‘any value’, done: false }

next().value will return the value for that particular statement.
next().done is a boolean which tells us if the generator function is done or still has other yield statements to run.

This example should clear things up a bit.

Jest and Enzyme

I will be using our very popular Jest and Enzyme to write our saga tests. I assume you already are familiar with them and have them set up. If not you can follow these links to setup Jest and Enzyme.

Example Saga and Action files

Here we have a scenario where we are fetching a list of authors from an api and saving them to our store. Let’s look into our example files for both first and then we will dive into testing them.

These will be our dummy action creators.
The saga that consumes and uses the above defined action creators.

There are a couple of ways we can test our sagas.

1. Step by Step approach

Our sagas being generator functions always yield effects which are saga factory functions like takeEvery, put, call etc. We can test each yield statement one by one by calling them using next() and asserting the returned values to the expected effect. This way of testing them is more suitable for smaller and simpler generator functions like fetchAuthorsFromApi from the above example.

Here in the first step, our yield calls a saga effect called takeEvery. So our generator object’s first iteration is asserted to the takeEvery effect with the required params.
As we only have a single yield statement, our second iteration of the generator object will return { value: undefined, done: true } , so we are expecting the same.

2. Testing the full saga

Although it can be useful testing our sagas step by step as described above that may result in brittle tests for complicated cases as per the official docs. A more full proof way of testing them would be to actually run them and test that the expected effects have occurred.

Our saga function that we want to test has an error handling try catch block where we fetch our authors using requestAuthors api and dispatch success or failure actions depending upon the api response.

function* makeAuthorsApiRequest(){
try {
const authors = yield call(Api.requestAuthors);
yield put(saveAuthorsToList(authors));
} catch (err) {
yield put(saveAuthorToListError();
}
}

We have used saga effect call to call our api and another effect called put to dispatch our actions.
Let’s test our success flow by mocking that api function requestAuthors so that it responds with a resolved promise using jest.spyOn method. We are returning a dummy authors object as well.

const dummyAuthors = { name: 'JK Rowling' };
const requestAuthors = jest.spyOn(api, ‘requestAuthors’)
.mockImplementation(() => Promise.resolve(dummyAuthors));

Then we will need to run our saga as a whole to test its effects.
Redux saga has an external api called runSaga which can be used to do exactly that.

Syntax: runSaga( options, saga, …args ) where 
- options : Object which supports options like dispatch, getState etc.
- saga: Saga function you want to run
- args: Array of arguments you want to send to the Saga function.

We will need to await for the saga to run and complete and then test our assertions. In our case, our options object will have a dispatch method as our saga uses put effect to dispatch actions and we want to test that. We will have an empty dispatched array which will get populated every time a dispatch is called by our saga. So let’s run our makeAuthorsApiRequest saga. The args params will not be needed as we don’t have any arguments to pass.

const dispatched = [];const result = await runSaga(
{
dispatch: (action) => dispatched.push(action)
},
makeAuthorsApiRequest
);

Now we can make our assertions. We can expect for our api requestAuthors to have been called. Also we can check our dispatched array’s first item to deep equal saveAuthorsToList action creator called with the dummy authors data from our mocked response.

expect(requestAuthors).toHaveBeenCalledTimes(1);
expect(dispatched[0])
.toEqual(saveAuthorsToList(dummyAuthors));

Let’s look into a complete example below. You can see cases for testing the catch block in our saga as well.

So this way we can run our saga and instead of testing each yield we are testing the mocked dispatched actions and api calls. You can find more examples here in the official documentation as well.

--

--