Testing Async Redux Actions with Jest

August 19, 2017
Alvin Crespo
Senior Developer

I recently ran into a problem with testing out asynchronous actions in react-redux. I didn’t quite know what I needed to do to expect the proper result of a successful async promise inside of an action. As a result, I had to dig through quite a few articles to piece it together. It’s my hope that with this article I save you some time and hopefully get you going in testing out your async actions in react with react-redux.

Our app code

There’s quite a few pieces that made this problem a bit complicated, and that included:

  • The use of a third-party script (Firebase)
  • Using react-redux with redux-thunk (Setting up a mock store)

Here is an action that is used when a person clicks the “Sign In” button in my app:

// src/actions/auth-actions.js

import * as types from './action-types';
import { auth } from '../services/db/firebase';

export function onSignIn(user) {
  return { type: types.ON_SIGN_IN, user: user };
}

export function onSignInError(error) {
  return { type: types.ON_SIGN_IN_ERROR, error };
}

export function signIn(email, password) {
  return (dispatch) => {
    auth
      .signInWithEmailAndPassword(email, password)
      .then((user) => dispatch(onSignIn(user)))
      .catch((error) => dispatch(onSignInError(error)));
  }
}

We want to examine the signIn function. This function returns another function that executes auth.signInWithEmailAndPassword. We’ll come back and examine the authentication module. But this code essentially dispatches the onSignIn method on success and dispatches onSignInError on failure.

Preliminary test setup

To properly test this function, we want to test that dispatch gets called with onSignIn and is passed the user object returned by the response. To be honest, I wasn’t quite sure how to do that yet, so I researched several articles and pieced it together:

// tests/actions/auth-actions.test.js

import { ON_SIGN_IN } from '../../src/actions/action-types';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import initialState from '../../src/reducers/initial-state';

describe('signIn', () => {
  it('dispatches the onSignIn action on success', () => {
    let middlewares = [ thunk ];
    let mockStore = configureStore(middlewares);
    let store = mockStore(initialState);

    store.dispatch(signIn('the.batman@dc.com', 'test1234'));

    expect(store.getActions()).toEqual([ { type: ON_SIGN_IN, user } ]);
  });
});

Let’s take a close look at our signIn test above.

  • First, I’m importing the action type I’m going to test against.
  • Next, I import configureStore, thunk and initialState so that I can setup a mock store to dispatch the signIn action.
  • Then, I define my middlewares and pass that into the configureStore call to create a mockStore method.
  • Then, I create my mocked store with the initial state of the app.
  • Finally, I dispatch the signIn action with an email and password to ultimately test against the actions that were called.

The idea here is that store.getActions() will give us the details on what actions were dispatched after the signIn call. We’re then evaluating the response of the signIn action which should be [ { type: ON_SIGN_IN, user } ].

But, this test fails. Welp.

It fails because we haven’t taken into account two items:

  • The third-party script (Firebase)
  • That we’re working with promises

So let’s first figure out how to deal with third party scripts.

Mocking out third party implementations

Remember that authentication object that we were importing:

// src/actions/auth-actions.js

import { auth } from '../services/db/firebase';

This is what the firebase module looks like in this app:

import firebase from 'firebase';

const config = {
  apiKey: process.env.FIREBASE_API_KEY,
  authDomain: process.env.FIREBASE_AUTH_DOMAIN,
  databaseURL: process.env.FIREBASE_DB_URL,
  projectId: process.env.FIREBASE_PROJECT_ID,
  storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
};

firebase.initializeApp(config);

let database = firebase.database();
let auth = firebase.auth();

export { database, auth };

The above firebase module configures and initializes firebase with the values of some environment variables. The important part here is that we initialize a database and auth object and export out those instances from this module.

We don’t really care about firebase in our tests. In fact, we don’t want to interface with firebase. So we need to mock that sucker in our tests. But, how do we do that? With Jest! Jest comes with mocking out of the box. Let’s see how this is done.

// tests/actions/auth-actions.test.js

import { ON_SIGN_IN } from '../../src/actions/action-types';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import initialState from '../../src/reducers/initial-state';
import { auth } from '../../src/services/db/firebase';

describe('signIn', () => {
  it('dispatches the onSignIn action on success', () => {
    let user = { firstName: 'Bruce' };

    auth.signInWithEmailAndPassword = jest.fn(() => {
      return Promise.resolve(user);
    });

    let middlewares = [ thunk ];
    let mockStore = configureStore(middlewares);
    let store = mockStore(initialState);

    store.dispatch(signIn('the.batman@dc.com', 'test1234'));

    expect(store.getActions()).toEqual([ { type: ON_SIGN_IN, user } ]);
  });
});

Here is the signIn test again, but it’s been updated with the following:

  • Imports auth from our firebase service
  • Sets auth.signInWithEmailAndPassword with a mocked function
    • Which returns a Promise that resolves with a user object

So now we have our firebase implementation mocked properly. But this test still fails! Why?

Using Async/Await

Let’s go back and see what auth.signInWithEmailAndPassword does in our app code:

// src/actions/auth-actions.js

    auth
      .signInWithEmailAndPassword(email, password)
      .then((user) => dispatch(onSignIn(user)))
      .catch((error) => dispatch(onSignInError(error)));

It’s a promise chain. But our test is currently setup for synchronous procedures. So we need to configure our test to execute in an async fashion. We do this by utilizing async/await, defining our test as an async function and awaiting on the dispatch, like so:

describe('signIn', () => {
  it('dispatches the onSignIn action on success', async () => {
    let user = { firstName: 'Bruce' };

    auth.signInWithEmailAndPassword = jest.fn(() => {
      return Promise.resolve(user);
    });

    let middlewares = [ thunk ];
    let mockStore = configureStore(middlewares);
    let store = mockStore(initialState);

    await store.dispatch(signIn('the.batman@dc.com', 'test1234'));

    expect(store.getActions()).toEqual([ { type: ON_SIGN_IN, user } ]);
  });
});

As you can see we now define the it block function as an async function:

it('dispatches the onSignIn action on success', async () => {

And we await the store.dispatch:

await store.dispatch(signIn('the.batman@dc.com', 'test1234'));

This ensures that the expect is run after the promise is resolved by our mock.

We can now run our tests and see that this passes. You successfully know how to test your async react-redux actions with ease. If you have any problems or questions feel free to ping me at @alvincrespo on Twitter. Cheers!

Bonus!

Need to mock many methods from an npm package?

Jest has a handy function called genMockFromModule.

const reactNativeDeviceInfo = jest.genMockFromModule(
  'react-native-device-info'
);

reactNativeDeviceInfo.getUniqueID = jest.fn();

Need to mock that library in every test?

Simply create a file in the __mocks__ folder with the same name:

// __mocks__/react-native-device-info
'use strict';

const reactNativeDeviceInfo = jest.genMockFromModule(
  'react-native-device-info'
);

reactNativeDeviceInfo.getUniqueID = jest.fn();

module.exports = reactNativeDeviceInfo;

References

Tutorial For Adding Redux to a React App

Stack Overflow: How can I mock an ES6 module import using Jest?

API testing with Jest

Stack Overflow: jest redux-thunk test if action of same module is dispatched

Jest Documentation: jest.spyOn(object, methodName)

Jest Documentation: Mock Implementations

redux-mock-store Documentation

MDN: Promise

MDN: await

MDN: async

react-redux Documentation: Usage With React