Unit Tests for Redux Sagas and APIs

Hi, I recently had to write test cases for my Redux Sagas and APIs and it started getting quite confusing. Good news is I could finally finish the task in hand. Yessss!!!…. but it took some time. Hence I’m writing this article to help developers who might already have or will stumble upon the same.

Moving on, so, when we are trying to write good code, our focus is generally on topics like logic, patterns, complexity, indentation, variable names etc. But one important aspect that we normally tend to forget is how our code can be structured or modularized so that it can be easily tested.

Where I’m going with it is, when I started working with Redux sagas I used to keep my APIs and Sagas in one file. Why? coz it was easier for me to understand the code and keep the code concerned with “same task” in one place. For eg.

But when I started writing test cases for the above snippet, I slowly started to realize how important it is to maintain separation of concerns. APIs and Sagas compliment each other but they are different and deal with different issues.

As per the above code snippet, Saga’s task is to

a. Capture a dispatch.

b. Call the generator function.

c. The generator function will call a method which would return some value and then finally trigger a dispatch with that value.

Whereas the API function’s task is straight forward: make a network call, here using axios, and then return a value based on response from network call.

So lets create two files mySaga.js and ApiCallHelper.js. As the name suggests, mySaga would have my saga functions and APICallHelper will have my API function. Hence, mySaga.js would look like this,

And my ApiCallHelper.js would look like this,

Now that we are done with separation of concerns, lets start writing test cases for them. At first for our saga, lets create a new file mySagaTest.js. Now before I do that, a disclaimer, I’m not going to use additional 3rd party library to test sagas, we will use what redux-saga library provides us to do it i.e {runSaga}.

In the above code snippet, we will start by importing the required libraries. Notice at the end, we import the generator function — not exampleSagaFunction, the function which actually has the Saga function. Why? coz like how we mock(read stub)methods in our test cases, we are going to use runSaga to simulate the same behaviour as that of our exampleSagaFunction, i.e to invoke the generator function.

Explanation:

4. In the next line we use await to await for our runSaga function to complete. RunSaga, accepts our fakestore, our generator function — getDataFromAPIGenerator, and args, here an action object with type and payload. Irrespective of type value provided, runSaga will trigger the getDataFromAPIGenerator. This becomes useful in cases where we have some sort of dependency on payload and the only way we can pass payload is via args parameter of runSaga.

5. As explained in redux-saga official docs, runSaga allows starting sagas outside the Redux middleware environment. Useful if you want to connect a Saga to external input/output, other than store actions. So when we pass the above parameters to it, runSaga invokes our generator function getDataFromAPIGenerator. This generator function would then call our stubbed API method and finally trigger a dispatch.

6. The dispatch method which gets invokes, is the dispatch method sitting inside our fakestore. It is mapped together by runSaga. Hence any dispatches happening via our runSaga would get stored in our dispatchedActions array.

7. Finally coming to assertion! As mentioned in the above point number 5, two things need to be asserted:

a. Our stub should get called via our generator function.

b. dispatchedActions array should have the dispatched action triggered by our generator function. Hence we do the following assertions and restore our stub.

So what we technically tested for our Saga is that when our generator function is called,

Now that we are done writing test cases for our Sagas, let’s move on to writing test cases for our API function ApiCallHelper.getDataFromAPI.

At first, let us start by creating a new JS file ApiCallHelperTest.js.

For testing our API method, we need to install a 3rd Party library called axios-mock-adapter. It’s an Axios adapter that allows us to easily mock requests.

Now we will test out two scenarios i.e Success, when status is 200 and Failure, when status is not 200.

Similar to our SagaTest file we import the necessary libraries. Then we create a MockAdapter using axios-mock-adapter.

Inside the it function’s callback method, we create a new mock object using MockAdapter. It accepts axios as parameter.

This mock object allows us to return status code and response by providing us with methods like onGet().reply or onPost().reply. We use onGet/onPost depending on the method type used in our actual API function. In our case, since we are using GET, we use onGet here.

So what we are telling with this code is, whenever we get any network call from our function, having method: “GET”, reply with status code 200, i.e Success, and response object {value: “delta”}.

For failure test case scenario we would return status code 500 and an error object. As you can see using MockAdapter makes it very easy to test various scenarios based on status codes or responses.

One we know is Network calls are async in nature. Hence, these functions can be tested in two ways(that I know of ). One, is already explained above while testing Sagas i.e using async in “it” function’s callback.

Second way of testing an async method is shown above where we use return to wait for our n/w call to complete. Once resolved we use .then to get the response, like how we do it for Promises and then make assertions on the response fetched.

In the first case, i.e in Success scenario, we are using mock.onGet().reply to return status code 200. In this case the method getDataFromAPI should return true. Hence, all that we need to assert here is that the response object is not null and response is true. If you are returning something else, then assert for that value by extracting that data out of the ‘getDataFromAPI’s response. All of that can also be done.

Similarly, for failure scenario, we are using mock.onGet().reply to return status code 500. In this case getDataFromAPI should return false. Hence, the only thing we would change in assertion is to check if response is false.

To summarize, while writing test cases for our our API methods we should at least assert for two things:

and then use the response from .then to make the required assertions.

The scenarios covered above are very basic but I feel it covers the basic fundamentals. Using this as our base we can always add more test scenarios.

I hope you found it to be useful and helpful. Feel free to comment back.

Happy coding… :)

Been working on JS for the last few years. At present I’m a SSE at Intuit focusing predominantly on UI. www.linkedin.com/in/aniket-paul-0400279a

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store