Oleg
Posted on July 14, 2020
Redux is an extremely useful library that facilitates managing of an app's state. Among many middlewares, Redux-Saga fits me the best as in a React-Native project I'm working at the moment, I've had to deal with lots of side effects that would bring me endless headaches in case I put them in components. With the tool, the creation of complex flows becomes straightforward. But what about the testing? Is this as smooth as the usage of the library? While I can't give you the exact answer, I'm to show you the real example of problems that I faced.
If you're not familiar with testing of sagas, I recommend reading a dedicated page in the docs. In the following examples, I use redux-saga-test-plan as it gives me the full strength of integration testing alongside with unit testing.
A bit about unit testing
Unit testing is nothing more than testing of a small piece of your system, usually a function, that has to be isolated from other functions and, which is more important, from APIs.
Going ahead I'd say that I haven't seen the point of unit testing in my project yet. I moved all the business logic and APIs abstractions into external modules to let the sagas handle only the flow of the application. So I haven't had those big sagas that I couldn't split into smaller, clearly see what they do (they're quite self-explanatory).
There you can see the usual way to check our effect creators. If there had been any API calls that, I would've mocked them using jest.fn
.
As we've done with tedious work, let's proceed to the main course.
Integration testing
The significant drawbacks of unit testing are external calls. You need to mock them. In case your sagas consist only of these calls and no logic, testing step by step, while abstracting all dependencies, becomes a dull task. But what if we only want to check the flow without dealing with each of the effects. What if we need to test sagas in the context of the state, with the use of reducers. I have excellent news, that's exactly what I wanted to share with you.
Test multiple sagas
Let's consider the following example, which is an adapted version of the code from my project:
Here we have the root saga sessionWatcher
that initialize application by calling initApp
right away after loading and also waits for the action to load a project by id. The project is loaded from storage after which, we save the project to the state and call an external function that saves the session and loads a map. The example shows all sorts of problems we can stumble upon during the testing process: working with multiple sagas, accessing the state, calling APIs that we'd like to avoid.
The test above tests all the sagas and split into few parts. The first part introduces objects that we'll use to test our sagas, the second, is testing itself. We call expectSaga
that'll run the root saga and tests it against the checks listed below it.
The first function we see is provide
, which uses matchers to locate the effect creators that will be mocked. The first tuple (pair of values) uses the effect creator from the Redux Saga library and matches exactly to select
effect that calls getProjectFromStorage
selector. If we want more flexibility, we can use matchers that are provided by Redux Saga Test Plan library as we do in the second tuple, where we say to match by function, ignoring its arguments. This mechanism allows us to avoid accessing the store or calling some functions and much, much more that I don't list here.
After that, we have a chain of effect cheks. Note that we don't have to put them in the specific order or include all the effects but rather list the effects that we expect to see. However, calls to dispatch must be in order.
The chain ends with silentRun
function that does three things: runs our test, suppress timeout error and return a promise.
Simulate an error
To simulate an error, we can use already familiar providers and a helper function from redux-saga-test-plan/providers
to replace an effect with an error.
Reducers and state
But what about the state, how would we test all together with reducers. With the library, the task is a no-brainer. Firstly, we need to introduce our reducer:
Secondly, we change our test a bit by adding withReducer
, which allows us using dynamic state (you can provide state without reducer by calling withState
), and hasFinalState
, which compares the state with the expected one, functions.
More on using the magic library here. Hope you enjoyed the reading, thank you.
Posted on July 14, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.