Testing asynchronous redux actions and reducers using Jest.

Published Feb 13, 2018Last updated Mar 28, 2018
Testing asynchronous redux actions and reducers using Jest.

Congratulations! You have gotten to that crucial point in the development of your amazing application where you are considering writing tests for your actions (this is me assuming you aren’t following the dogmatic TDD approach). In this tutorial, we are going to walk through the process of writing tests for our redux actions (most of which are asynchronous in nature) and our reducers. We would start from the ground up, from setting up and configuring jest to writing tests for our actions and reducers. So let’s dive in!
First thing we would need to do is install a few packages we would need for our test purpose. Run the following mouthful of a command to get those packages downloaded and installed:
npm i -D jest moxios redux-mock-store expect
Here, we are installing the jest package for our test suites, moxios to mock API calls in our actions, redux-mock-store to, well you guessed it, mock our redux store and expect to make assertions in out tests. With all these installed, let’s go ahead to configure jest.
Create a file called jest.config.js in the root of your application and write the following line of codes in it:

module.exports = {
 testMatch: [
   '**/?(*.)(spec).js?(x)'
 ],
 testPathIgnorePatterns: [
   '<rootDir>./server/__test__/*.js'
 ] };

What we are doing here is:

  1. Specifying to jest that we want it to run every file that ends with .spec.js in its name.
  2. Telling it to ignore our server side tests

Finally, add the following script to your package.json file
"test-client": "jest --config=jest.config.json"
A little sidenote: This configuration is not required if all of your test suites are in one directory. By default, jest would look in directories named test and run every javascript file in that directory. Sometimes, you might have a separate test directory for the server-side and client-side parts of your application and as such, would not want Jest running your server-side test when you need it to run just your client-side test.

Now to the fun part - Writing some code. Assuming we have the following action in our actions directory:

import axios from axios
import { ADD_RECIPE } from ‘./types’

export const addRecipeActionCreator = recipe => ({
  Type: ADD_RECIPE,
  recipe
});
export const addRecipeAction = (recipeData = {}) => {
 return (dispatch) => {
   const {
     name = '',
     description = '',
     img_url = 'no-img-here',
     category = '',
     ingredients = '',
     instructions = ''
   } = recipeData;

   const recipe = {
     name,
     description,
     img_url,
     category,
     ingredients,
     instructions,
   };
   return axios.post('/api/v1/recipes', recipe)
     .then(() => {
       toastr.success('Recipe added successfully.');
       dispatch(addRecipe({
         id: recipe.id,
         ...recipe
       }));
     })
     .catch(error => Promise.reject(error.response.data.message));
 };
};

A quick overview of what we have here: an action creator that specifies what type of action it sends to the store and an action that “dispatches” it to the store. Easy, yeah? Next, we have our reducer for this particular action:

const defaultState = {
  recipes: []
};

export default (state = defaultState, action) => {
  switch(action.type) {
  case ADD_RECIPE:
  return {
  ...state,
  recipes: [...state.recipes, action.recipe]
    }
  }
}

So basically, what we have here is our default store state being changed depending on what action gets to the reducer. Note that the state is never mutated. Easy peasy. Now that we have a base understanding of what an action looks like, let’s get some test in there. Since our actions need data to be dispatched to the store, we would need to have some dummy data we can send through to our actions. Create a file called recipes.js.

const recipe = {
 name: 'Amazing fried rice',
 description: 'A really amazing fried rice',
 imageUrl: 'https://somereallycoolimage.ly',
 category: 'Lunch',
 ingredients: 'Bread\nSamolina\nBeancake',
 instructions: 'Cook it really well\nAvoid mixing with water',
};
export const recipeResponse = {
 status: 'Success',
 recipeData: recipe
};

Next, we write some tests for our actios.

import moxios from 'moxios';
import thunk from 'redux-thunk';
import configureMockStore from 'redux-mock-store';
import expect from 'expect';
import { addRecipeAction } from ‘../action/recipes’;
import { ADD_RECIPE } from '../action/types';
import { recipeResponse } from ‘./__mock__/recipes’;

const mockStore = configureMockStore([thunk]);

describe(‘Recipe actions’, () => {
beforeEach(() => moxios.install());
 afterEach(() => moxios.uninstall());

describe('Create Recipe Action', () => {
   it('Should dispatch ADD_RECIPE action and add a recipe', async (done) => {
     moxios.stubRequest('/api/v1/recipes', {
       status: 201,
       response: recipeResponse
     });
     const returnedAction = [{
       type: ADD_RECIPE,
       recipe: recipeResponse.recipeData
     }]; 

     const store = mockStore({});
     await store.dispatch(addRecipeAction({
       ...recipeResponse.recipeData
     }))
     expect(store.getActions()).toEqual(returnedAction);
     done();
   });
 });
});

Here, we import the packages we require for this test-suite to be successfull. Then, we import our actions and mock data. We then mock our store using redux-mock-store package. Using moxios, we are able to mock a request to our API. If everything goes according to plan, we are asserting that the actions we have defined would return the same result as the mocked actions we have in our test suites. And that’s it. Run the command npm run client-test. Your one and only test-suite should be all green. Yaayy! Congratulations, you now know how to write tests for asynchronous redux actions.
Same thing applies to our reducers also. We want to create a mock response for our reducers in a separate directory inside the __mock__ folder. Create a file named recipes and fill it with the following lines of code:

export const mockRecipesReducers = {
 name: 'Amazing recipe',
 description: 'Amazing recipe here',
 img_url: 'https://someimagehereforall',
 category: 'Lunch',
 ingredients: 'Amala\nEwedu',
 instructions: 'Cook the meal well\nTurn properly',
 owner: 1
};

And our reducer tests file would have the following lines of code:

import expect from 'expect';
import deepFreeze from 'deep-freeze;
import { mockRecipesReducers } from '../__mocks__/actions/recipes';
import recipeReducers from '../../reducers/recipes';
import * as types from '../../actions/types';

describe(‘recipe reducer’, () => {
  it('Should return the initial state', () => {
   expect(recipeReducers(undefined, {})).toEqual({
     recipes: [],
     userRecipe: [],
     singleRecipe: '',
     favoriteRecipes: [],
     userFavoriteRecipesId: []
   });
 });
it('Should handle ADD_RECIPE action', () => {
    const stateBefore = {
      pages: 0,
      recipes: [],
      userRecipe: [],
      singleRecipe: '',
      favoriteRecipes: [],
      userFavoriteRecipesId: []
    };
    const action = {
      type: ADD_RECIPE,
      recipe: [mockRecipesReducers]
    };
    const stateAfter = {
      pages: 0,
      recipes: [mockRecipesReducers],
      userRecipe: [mockRecipesReducers],
      singleRecipe: '',
      favoriteRecipes: [],
      userFavoriteRecipesId: []
    };
    deepFreeze(stateBefore);
    deepFreeze(action);
    expect(recipeReducers(stateBefore, action)).toEqual(stateAfter);
  });

});

A quick walkthrough of what we have going on here: We mock the data we expect to get when our action gets dispatched to the reducers on its way to the store. Our first test asserts that the initial state of our redux store would be returned un-mutated when no data is sent through. Our second test asserts that the state we would be left with after our reducers have been modified is the same as the mocked reducer state. We make use of deepFreeze to ensure that no mutations occur in our reducers and that our reducer is a pure function that never mutates the state. And that’s basically it. This might be a lot of information to wrap your head around if you are testing for the first time, but do not worry. Understanding it might take time and you most certainly might run into errors on your way to getting your tests to pass but that’s all part of the process! With this information at your disposal, you should be able to test any asynchronous action and reducer you have written. Cheerios!

Discover and read more posts from Ayelegun Kayode Michael
get started