Unit testing with Jest for redux-saga in React native

Tram Ho

Introduction

Hello friends. Unit testing has long been an indispensable part of testing the software we write as good as expected or not. Side-effect related functions such as API requests should be written as tests because of the importance of strict logic. For native React apps, we often use the Redux saga middleware to handle this side-effect. There are many methods for writing unit tests with saga. Today I will introduce quite simple methods, using only very basic methods and libraries from redux-saga and Jest .

A bit about the redux-saga and the generator function

If you forgot the redux-saga, you can review it here

When using redux-saga, we will declare the saga in the generator function ( function* ). That type of function can be executed, pause returns results and continue to execute thanks to the keyword “yield”. The Generator function returns an iterator object that has a next() to get the results returned at the point where the iterator is paused.

As a result, the generator function has the ability to pause before terminating and can continue to run at another time. For example, if you need to get any value in the store before calling the API, or read some file in the app’s directory and then go to call the API, this time the generator function helps us to catch the disagreement. jogging synchronously. So when testing we can also call to check the results in the order we want.

Jest

Jest is a Javascript library, and is seen as the default test library for React apps. Although Jest can also be used to test UI components, in this article I will only talk about functional testing. In addition to providing a mechanism to call a defined function, Jest also helps us to fake functions referenced from a module or a file. If your test function calls some other asynchronous function, fake support of these asynchronous functions will help you temporarily pass them more focus on the function you are testing. There are many documents written about Jest, you can find and read more with Jest library here

Just rambling like that, now let’s go to the main part.

Test a simple generator function

Let’s take the simplest example for the generator function as follows:

This function has 2 keywords yield . When declaring an iterator object to call that function with the syntax:

At this point, the function has not started to execute. We have to call the iterator’s next () method, then it will actually run until it comes to yield, it will stop at that statement.So The first time the function will stop at line 1 and return the main result is i , the next call next () will return i + 10 and still stop at line 2. We have to call next() again, this time the iteratior object will return done results at the same time. This generator will terminate.

Then we check is also very simple as follows:

Test a watcher of a Saga

One of our familiar syntax after defining a saga is that we will need a watcher to observe an action in order to execute the saga. Let’s say we have a saga calling API that gets a list of devices. The first are the actions for this request as follows:

Now we have a watcher observing the action type GET_LIST_DEVICE and a saga that executes the API call, if successful we will put action getListDeviceSuccessAction() and if an error occurs we will put action getListDeviceErrorAction() . Here we are assuming that the Api.requestDevices () function is a promise

Now we will see if watcher deviceListSaga observes the GET_LIST_DEVICE action and calls getListDevice function.

Because our watcher is just a generator function, when initialized

then nothing has happened yet. When we call next the first time, it will execute and will return the value which is the takeLatest effect and not finished. The second call next () actually terminates this function.

Test an entire Saga

Now, we need to run the getListDevice saga (still just a generator function). When running a generator function, meeting the yield keyword, the iterator will stop, so now we will use the runSaga method of redux-saga itself to run the entire thread of getListDevice . However, when running to yield requestDevices(categoryId) , because this is a promise, the iterator will not know what result will get. To make sure the entire thread runs out, we have to make requestDevices() pass through. In the example above, the promise requestDevices(categoryId) is either successful or an error occurs. We’ll test the success case first, by requestDevices(categoryId) Promise requestDevices(categoryId) returns a resolve ():

And now let’s examine all of the saga together. The syntax of runSaga( options, saga, …args ) includes:

  • options : is an object that helps us define options such as dispatch, getState, channel …
  • saga : is the saga we want to test
  • args : the parameter for the saga we need to test

We will wait for the runSaga to finish running in an async function before going to check the desired results. In this example, we have configured the option:

Since the effect put(action) is the dispatch(action) of redux, when running saga, if any effect put aciton is called, we will save that action in the array and then end the process then check if the desired action is obtained. call or not. And after the runSaga finishes, we test the action called by

If you want to mock Promise requestDevices(categoryId) return an error, we just need as simple as follows:

Among the saga you will probably refer to the store to get something value before proceeding. So now we will call runSaga with the option to add getState to ensure the entire thread will run as expected.

You probably didn’t know it yet – when used with typescript

Error when not pressed for saga style

To make it more concise, the above code I have not written Typescript. If the project you use Typescript, then please manually modify and declare the type for the syntax in the saga. And if you call runSaga with the above syntax and get the type error in your saga as follows Argument of type '({ <arg> }: <Action>) => Generator<Promise<any> | PutEffect<> | PutEffect<>, void, [...]>' is not assignable to parameter of type 'Saga<any[]>'. Then press your saga into Saga<any[]> now

How to test a saga containing another saga?

I am sure that in your saga processing, you will need to call another saga (or generator function), such as the following

With the generator function removeImage is declared somewhere in your project:

At this time removeImage() is a generator function you write, not Promise as the original example. No matter how many other saga calls in this saga, in short, you just need to pass through the commands that call them to continue running our main Saga. So instead of mocking a Promise, we also just need to mock the saga to return the expected results, right? Now I will declare the removeImage mock so that the editDevice saga editDevice run smoothly:

Then continue to simply use jest to mock Saga only

Conclude

This post is based on my team’s personal experience applied to the react-native project. Hope this post can be of some help for you in unit test array with redux-saga for react app. In the article, if you make a mistake, please point it to me. Thank you for reading.

Reference source:

https://medium.com/@13gaurab/unit-testing-sagas-with-jest-29a8bcfca028

https://redux-saga.js.org/docs/advanced/Testing.html

Share the news now

Source : Viblo