Testability in Redux

Thunk vs Loop vs Saga

Thon Ly
Silicon Wat University
6 min readOct 23, 2017

--

Photo: https://github.com/reactjs/redux/tree/master/logo

When you use my referral link above 👆 to become a Medium member, all proceeds will be donated towards the construction of the Silicon Wat Campus for children in Ukraine and Cambodia ❤️

Introduction

Redux is a predictable state container, especially for React applications. Like React, Redux also has a small API surface. To master Redux is to master patterns, not API abstractions. The essential pattern in Redux mirrors the MVC pattern of separating the Model, from the View, from the Controller. In the vernacular of Redux, this translates to reducers, action creators, and middlewares, respectively.

In Redux, a single state object manages the entirety of an application, no matter how large or complex. For big projects, the main state can be splitted into many little slices. This means the root reducer can be composed from smaller ones, each managing one branch of the state tree. Such a pattern makes inspecting and debugging, hydrating and persisting of state easy. In fact, “time-travel” debugging becomes possible and trivial.

Reducers specify how actions transform the state tree. To change the state is to emit an action. An action is simply an object that describes what happened, while action creators are factories that return those actions. When an action is dispatched, it passes through a middle layer before reaching the reducer. Inside this middle layer, middlewares can be created to intercept actions and attach business logic.

Altogether, reducers, middlewares, and action creators separate the state from the logic from the calling events, respectively. They are all functions that should be kept pure. When a function is pure, the same input is always guaranteed to result in the same output. A one-to-one relationship between input and output makes functions predictable and easily testable. In practice, however, not all functions can be pure, especially asynchronous ones. This is unfortunate because the web is essentially asynchronous in nature. To address this problem, ES6 allows the creation of generators that makes asynchronous code behave in a synchronous fashion. The effect, asynchronous functions can become pure and thus trivial to test!

To fully appreciate this new state of the art, let’s first examine the most popular pattern in current practice: the Thunk

If you’re looking for A Complete Frontend Developer Course for Beginners, check out this textbook written by Thon Ly exclusively for Medium:

To help you achieve full mastery, all three web languages (HTML, CSS, and JavaScript) are taught together in parallel within a musical context in order to deepen your understanding of their interrelationships in a fun and memorable way!

Thunk

A thunk is the most popular pattern to make asynchronous API calls possible in Redux applications. Essentially, a thunk is a function that returns another functiona higher-order action creator to be exact. Instead of dispatching actions, the concept is to dispatch functions which then dispatch actions. This way, business logic can be inserted (such as making API calls) before letting actions propagate. These additional logics are known as side effects in the Redux idiom.

For example, take a look at this API that retrieves a user’s GitHub profile:

In Redux, this usually means creating three action creators that correspond to the three states of an API call: fetching, success, and fail

Then, we can create a reducer that updates the state accordingly:

In Redux, it’s crucial to update a copy of the state and return that instead of the original. Immutability is necessary because Redux shallowly checks for reference changes. Either way, immutable data makes data handling safer.

Now, we can create a thunk to call an API endpoint before dispatching the appropriate actions:

To make this thunk work, we need to register a middleware that watches for it:

Finally, we can make API calls simply by dispatching the thunk we created. Though this implementation has the desired functionality, it breaks one of Redux’s three core principles:

The only way to change the state is to emit an action.

Although actions are ultimately emitted to change our state, technically we’re emitting thunks instead of actions. This has the undesirable effect of coupling the logic too closely with the calling event, thus breaking the MVC pattern of separation. Take a look at this live application that connects our thunk to a React component:

Our action creators and reducer are all pure, so it’s straightforward to test:

The code to test our reducer:

However, our thunk is impure and therefore unpredictable. Moreover, its asynchronous code makes it notoriously difficult to test:

To fully test our asynchronous thunk, we need to create a mock API server. It’s non-trivial to implement, so forgive the author for ending the testing here! 😊

Loop

Similarly, Redux Loop reverses the pattern in thunk, coupling the logic too closely to the state.

TK

Saga

Thankfully, the saga pattern makes mock API servers irrelevant! A saga is a function that utilizes generators to control the flow of execution. Generators even have the ability to “pause” execution until an asynchronous call completes! In effect, asynchronous functions can be written to have very predictable behaviors. Such purity makes testing effortless, even if it involves making API calls to a backend!

To refactor our GitHub application to use the saga pattern, we replace our thunk middleware with this saga middleware (using Redux Saga):

Whenever the FETCH action is dispatched, this middleware will catch it. Then, the middleware will pass it to the getUserSaga:

Notice in line 3 that we can assign the result of a API call to a variable! Unlike the thunk, this saga pattern allows us to dispatch actions instead of functions, even though it involves asynchronous side effects. By abiding to Redux’s core principle of keeping actions pure, a saga decouples the logic from the calling event. Architecturally, this means sagas are contained inside the middle layer, or the “C” in the MVC pattern of separation.

Take a look at our new refactored application:

The saga pattern really shines when it comes to testing API calls:

A yield statement is incredible because we can insert any value in its place on the next call. Take a look at the second test in line 22. We can tell our generator function getUserSaga that we want the result of our GitHub call to be state.user! This one line makes mocking API servers obsolete! 😉

Moreover, we could even throw an error to catch the FAIL test case! Basically, what was once notoriously difficult is now trivial!

Reference: https://redux.js.org/recipes/writingtests

When you use my referral link above 👆 to become a Medium member, all proceeds will be donated towards the construction of the Silicon Wat Campus for children in Ukraine and Cambodia ❤️

Conclusion

The beauty of Redux lies in its patterns. Utility functions exist only to tie together those contracts. As a result, Redux has a refreshingly small API surface. The overarching pattern separates the state from the logic from the calling events that parallels MVC architecture. In the language of Redux, events dispatch actions that travel through middlewares before reaching the reducers. As such, Redux pairs nicely with “View” libraries such as React, where UI updates can be inferred from state changes, and components can easily dispatch actions to update the state and rerender. As the state grows larger for complex applications, reducers that manage it can be composed from smaller ones, much like composition of components in React. In Redux, it’s crucial never to mutate state, as doing so is an antipattern.

Ultimately, all this is done to ensure that functions are predictable and pure to make testing easier. Though a popular pattern, thunks are impure and fail to separate the “C” from the “V” in the MVC pattern. Similarly, loops fail to separate the “C” from the “M”. By utilizing generators, sagas are able to contain all the side effects inside the “C” in addition to making asynchronous code behave synchronously. Incredibly, testing asynchronous logic becomes trivial! More and more, quality is becoming the key differentiator, which depends heavily on accurate and precise testing. Testability in turn depends on a clean and clear separation of concerns.

If quality is important to you, testing should be too.

If you like this article, you might also like:

--

--