Robin van der Vleuten

Going epic with Redux Observable tests

The last few React projects I worked on used Redux Observable heavily. It is a good way to keep business logic out of components, but testing epics took me a while to get comfortable with.

So what is this Redux Observable?

If Redux Observable is new to you, start with RxJS + Redux + React = Amazing! by Jay Phelps. It explains how Netflix used common JavaScript patterns with RxJS to manage business logic in React applications. The core idea became the Redux Observable library.

Redux Observable Logo

The documentation has plenty of small examples to get you started. Testing is the part that felt less obvious to me. The docs even say they were still learning the best way to test epics:

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 fighting with observable tests on a few projects, this is the approach that started to make sense.

What epic are we going to test?

Here is a small epic we can use for testing asynchronous business logic:

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,
}
})
})
)
}

This epic authenticates a user with credentials from the dispatched action. The action might look like this:

js
export const authenticate = (username, password) {
return { type: 'USER_AUTHENTICATE_REQUEST', username, password };
}
dispatch(authenticate('johndoe', 'mysupersecretpassword'));

The client dependency is injected into the epic. You could import the client directly, but injection makes it much easier to mock in tests.

Creating the tests with Jest

Most React projects I saw around that time used Jest, so I will use it for the examples.

The test passes a dispatched action into the epic and checks the action that comes out. Looking at the epic, we need two tests: one for USER_AUTHENTICATE_SUCCESS with a JWT token and one for USER_AUTHENTICATE_FAILURE with an error.

In Jest, that starts like this:

js
describe('authenticateUserEpic', () => {
it('should dispatch a JWT token when authenticating is successful', () => {
// ...
})
it('should dispatch an error when authenticating has failed', () => {
// ...
})
})

Focus on the successful case first. We pass the epic a dispatched action and wait for the RxJS observable to complete. There are several ways to do this, but this version worked well 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 return
const 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',
},
])
})

The RxJS part takes some getting used to, but the test ends up being direct: put an action in, wait for the epic, assert the resulting action.

The failed response test follows the same shape:

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 return
const 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 get some headaches along the way? Definitely. I had a few questions before RxJS started to click. The Redux Observable community helped a lot, and the pattern gave me a useful way to structure React applications with less logic inside components.