Going epic with Redux Observable tests
- Published
The last couple of React projects I've developed where built with a lot of help from the Redux Observable library. It is an excellent library to separate your business logic from your components, but the correct way to test is still something they need to find out. In this article, I am gonna share my ideas on this topic.
So what is this Redux Observable?
For those who aren't aware with any of this library, I recommend to you to check out the RxJS + Redux + React = Amazing! talk by Jay Phelps. It's a very inspiring talk on how Netflix uses some common JS patterns combined with the powers of RxJS to manage your business logic within your React application. They've extracted the core from Netflix and shared it as an open-source library on Github.
Their documentation is excellent and contains a lot of small running examples to help get you started. The whole library deserves an article on its own, but one important aspect is still a bit underexposed. As a matter of fact, they are still struggling with the best way™ themselves;
Testing async code that creates side effects isn't easy. We're still learning the best way to test Epics. If you have found the perfect way, do share!
After struggling with the Observable tests on a couple of projects, I would like to give my two cents on the topic in this article.
What epic are we going to test?
To get a nice epic to show how you can test asynchronous business logic, I came up with the following;
js
export const authenticateUserEpic = (action$, store, { client }) => {// Only act on actions of a given type,// in this case "USER_AUTHENTICATE_REQUEST".return (action$.ofType('USER_AUTHENTICATE_REQUEST')// Map the received action to a new action "observable"..switchMap((action) => {// Authenticate with the dispatched credentials in the action,// using the injected client instance.return client.authenticate(action.username, action.password).then((response) => {if (!response.isSuccessful) {// Map the response to a "failed" action with the error.return {type: 'USER_AUTHENTICATE_FAILURE',error:'Something went wrong while authenticating',}}return {// Map the response to a "successful" action with a JWT token.type: 'USER_AUTHENTICATE_SUCCESS',idToken: response.idToken,}})}))}
As you may have noticed is this epic about authenticating an user with the dispatched credentials. I can imagine I would dispatch such action like this;
js
export const authenticate = (username, password) {return { type: 'USER_AUTHENTICATE_REQUEST', username, password };}dispatch(authenticate('johndoe', 'mysupersecretpassword'));
You may also have noticed that I've injected the client dependency into my epic. You could get a client instance through a require or import statement. But by using dependency injection it makes the client way easier to mock and your epic way easier to test.
Creating the tests with Jest
Most of the React projects out there seem to be using Jest, so I'll just use it in the example test.
My approach to test the above epic, is to get the expected action when the epic receives the dispatched action. So a quick glanse on the epic tells us that we need two tests; one where we expect USER_AUTHENTICATE_SUCCESS
with a JWT token and one where we expect USER_AUTHENTICATE_FAILURE
with an error.
To define them as Jest tests, one would define them as follows;
js
describe('authenticateUserEpic', () => {it('should dispatch a JWT token when authenticating is successful', () => {// ...})it('should dispatch an error when authenticating has failed', () => {// ...})})
So let us focus on the first test for now. We need pass the epic the dispatching action and get the resulting action when the RxJS Observable completes. There are many ways to write such code, but the following works the best for me;
js
import { ActionsObservable } from 'redux-observable'import authenticateUserEpic from './epics'// ...it('should dispatch a JWT token when authenticating is successful', async () => {// The response object we expect to receive from the server.const response = {isSuccessful: true,idToken: 'a-random-generated-jwt',}// Create a fake client instance which will returnconst client = { authenticate: jest.fn() }client.authenticate.mockReturnValue(Promise.resolve(response))// Create an Observable stream of the dispatching action.const action$ = ActonsObservable.of({type: 'USER_AUTHENTICATE_REQUEST',username: 'johndoe',password: 'mysupersecretpassword',})// Pass the Observable action to our action and inject the// mocked client instance.const epic$ = authenticateUserEpic(action$, store, { client })// Get the resulting actions by using async/await.const result = await epic$.toArray().toPromise()// Test if we've received the expected action as result.expect(result).toEqual([{type: 'USER_AUTHENTICATE_SUCCESS',idToken: 'a-random-generated-jwt',},])})
Not that hard right? You'll need to understand RxJS first. But after that, you will get a nice separation of concerns in your React applications. To make the examples complete, the following test will handle the failed response;
js
it('should dispatch an error when authenticating has failed', async () => {// The response object we expect to receive from the server.const response = {isSuccessful: false,}// Create a fake client instance which will returnconst client = { authenticate: jest.fn() }client.authenticate.mockReturnValue(Promise.resolve(response))// Create an Observable stream of the dispatching action.const action$ = ActonsObservable.of({type: 'USER_AUTHENTICATE_REQUEST',username: 'johndoe',password: 'mysupersecretpassword',})// Pass the Observable action to our action and inject the// mocked client instance.const epic$ = authenticateUserEpic(action$, store, { client })// Get the resulting actions by using async/await.const result = await epic$.toArray().toPromise()// Test if we've received the expected action as result.expect(result).toEqual([{type: 'USER_AUTHENTICATE_FAILURE',error: 'Something went wrong while authenticating',},])})
Did I got some headaches along the way? I definitely got some questions before I had a basic understanding of RxJS! But luckily the Redux Observable community was very helpful. And now I have a very valuable new tool to structure my React applications around 👌