Testing React-Redux App with Jest

We often get confused about selecting testing framework for our application. Currently, I am working on a ReactRedux based project. While selecting the testing framework, we compared some of the popular JavaScript testing frameworks. We found that Jest is the best fit for testing our application.

Jest is not limited to ReactJs testing. We can test any JavaScript code using Jest. It can be used to test asynchronous code.

In React – Redux project, we will have a single store containing state of the application. We will have actionCreators which return action type and payload(may be the response from API). Reducer will contain actual logic to update store for a particular action. Components listen to the reducer. So, when state of the reducer changes, component will be re-rendered.

Here, we will discuss how Jest helped us for testing actionCreators, reducers, components in our project. ActionCreators return the actions. So, we are not testing actions .

Suppose we have a file friendListActions.js which contains string literals for the actions:

const friendListActions = {
  fetchFriendList: 'FETCH_FRIEND_LIST',
  fetchingFriendListSucceeded: 'FETCHING_FRIEND_LIST_SUCCEEDED',
  fetchingFriendListFailed: 'FETCHING_FRIEND_LIST_FAILED'

};

export default friendListActions;

Suppose, we have following file friendListReducer.js.We are changing the state in the reducer based on the actions.

import friendListActions from 'friendListActions.js';

//Set the initial state for this reducer.
const initialState = {
  isLoading: false,
  errorMsg: null,
  friendList: []
};

//Here is our business logic to change state in the reducer.
const friendListReducer = (state = initialState, action) => {
  switch (action.type) {
    case friendListActions.fetchFriendList:
    case friendListActions.fetchingFriendListSucceeded:
    case friendListActions.fetchingFriendListFailed:
      return { ...state, ...action.payload }
    default:
      return state;
  }
}
export default friendListReducer;

Suppose we have the following file friendListActionCreators.js file containing action creators for fetching friend list. We are handling success as well as error response while fetching the friend list.

import friendListActions from 'friendListActions.js';

//This actionCreator is to initialise the fetching of friend list
export const fetchingFriendListInitiated = () => {
  //actionCreator is returning an action object.
  return {
    type: friendListActions.fetchFriendList,
    payload: {
      isLoading: true
    }
  }
}

//This actionCreator is used when friend list is fetched successfully.
export const fetchingFriendListSucceeded = ( friendList ) => {
  //actionCreator is returning an action object.
  return {
    type: friendListActions.fetchingFriendListSucceeded,
    payload: {
      isLoading: false,
      errorMsg: null,
      friendList
    }
  }
}

//This actionCreator is used when failed to fetch friend list.
export const fetchingFriendListFailed = ( errorMsg ) => {
  //actionCreator is returning an action object.
  return {
    type: friendListActions.fetchingFriendListFailed,
    payload: {
      isLoading: false,
      errorMsg
    }
  }
}

export const fetchFriendList = () => {
  return( dispatch => {
    dispatch( fetchingFriendListInitiated() )

    //Here we are fetching the friend list for user having 23 as id.
    return fetch("http://social-media.com/23/friends")
    .then(successResponse => {
      dispatch(
        fetchingFriendListSucceeded(
          successResponse.response
        )
      )
    })
    .catch(errorResponse => {
      dispatch( fetchingFriendListFailed(errorResponse.message))
    })
  })
}

We are making ‘fetch’ call to the respective API.

Let us observe the test cases for the reducer. We have the following file friendListReducer.test.js:

import reducer from 'friendListReducer.js';
import friendListActions from 'friendListActions.js';

const expectedInitialState = {
  isLoading: false,
  errorMsg: null,
  friendList: []
}

//'describe' is used to create 'test suite' containing multiple test cases.
describe('Friend List Reducer', () => {
  it('returns a state of reducer when succeeded to fetch the friend list', () => {
    let expectedPayload = {
      isLoading: false,
      errorMsg: null,
      friendList: [
          'John', 'Emraan', 'Sukanya'
      ]
    }

    expect(
      // "reducer" takes 2 arguments:
      // first argument: state of reducer before applying the action
      // second argument: Plain JavaScript Object containing "action" and "payload"
      reducer(expectedInitialState, {
        type: friendListActions.fetchingFriendListSucceeded,
        payload: expectedPayload
      })
    ).toEqual({ ...expectedInitialState, ...expectedPayload })
  })
})

State in the reducer should be changed when some action is performed. Here, we are testing whether this state is changing as per expected payload or not.

Here, we are using expect() and toEqual() methods provided by Jest. Also, it provides describe to create a test suite and it to create an individual test case.

Let us test which actions will be performed when friend list is fetched successfully from the API. Let we have file friendListActionCreators.test.js:

import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import * as fetch from 'jest-fetch-mock';

import friendListActions from 'friendListActions.js';
import { fetchFriendList } from 'friendListActionCreators.js';

//We are creating a mock store here.
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);

describe('fetchFriendListSucceeded()', () => {

  it('returns friend list in response', () => {
    const getFriendList = [
      'John','Emraan', 'Sukanya'
    ]

    const jsonResponse = {
      "method": "getFriendList",
      "response": getFriendList
    }
    
    //We are mocking only one http fetch response
    fetch.mockResponseOnce(JSON.stringify(jsonResponse));

    let store = mockStore({
      friendList: {}
    })

    let expectedActions = [
      {
        type: friendListActions.fetchFriendList,
        payload: {
          isLoading: true
        }
      },
      {
        type: friendListActions.fetchingFriendListSucceeded,
        payload: {
          isLoading: false,
          errorMsg: null,
          friendList: getFriendList
        }
      }
    ]
    //We are returning 'promise' due to asynchronous actions.
    return (
      store.dispatch(fetchFriendList())
      .then(() => {
        expect(store.getActions())
          .toEqual(expectedActions);
      })
    )
  })
})

Here, while testing, we should mock the Redux store. Since we are using fetch call to the API,our store is getting responses from asynchronous actions. As Redux only supports synchronous code, we require middle-ware f so that it supports asynchronous code. While writing test cases, we should mock middle-ware also.

Jest can’t mock http  fetch calls, Redux store and middle-ware which will be used in store. So, we need to add some other packages for mocking these things.

Here, we have used ‘redux-mock-store’ to create a mock store and ‘redux-thunk’ to mock the middle-ware. We are using ‘jest-fetch-mock’ package to mock http fetch calls. We have used ‘mockResponseOnce()’ method since we want to mock only one API call.

After mocking API call, it will return a static value. But, our actual code expects a promise object to be returned from API. So, for simulating the same behaviour, we are returning the promise object in the test case.

Snapshot testing:

When we don’t want to change UI components unexpectedly, Snapshot testing will be useful.Jest provides this  amazing feature.

Snapshot for the component is created when test case for the component is run for the first time. So, when I am running Snapshot test cases for the first time, they will pass successfully. This shows that Snapshot testing is not Test Driven Development(TDD). For making it TDD, we can use enzyme package along with it.

Jest creates a new folder __snapshots__ under the current working folder of the test cases and the snapshots will be stored here. These snapshots will be in human readable format. When I run test cases afterwards, the component will be compared with it’s existing snapshot. If some modifications are done in the component, test case will fail. If these changes are desired, we can change the existing snapshot.

Let us have a list component in list.js file:


import React, { Component } from 'react';

class List extends Component {

  render() {
    const flowers = ['Lily', 'Lotus', 'Rose']
    return (
      <div>
        <h2> List of flowers </h2>
        <ul>
          {
            flowers.map(( flower, index ) =>
              <li key={ index }> { flower } </li>
            )
          }
        </ul>
      </div>
    )
  }
}
export default List;


We will write test case for this component to create it’s snapshot:

import React from 'react';
import renderer from 'react-test-renderer'
import List from 'list.js';

it('renders list component correctly', () => {
  const tree = renderer.create(<List/>).toJSON()
  expect( tree ).toMatchSnapshot()
})

 

For creating snapshot of a component, first we have to create Json object of that component. So, we will create JavaScript object for the component and will convert it to Json. We are unable to create JavaScript object of the component in Jest. So, we have used ‘react-test-renderer’ .

Jest provides ‘toMatchSnapshot()’ method to create snapshot(if it is absent) for that component. Next time, when I want to test List component, it is compared with existing snapshot.

If there is any change in List component, the test case will fail. We can update the snapshot to reflect these changes in snapshot. We should commit these snapshot files along with other test files. You can find more information here.

If we want to add assertions in component testing, want to check manipulations in the components, we can use enzyme package along with Jest framework. enzyme will add TDD in out UI component testing.

In short, Jest will create component tree structure and we can traverse this component tree with the help of enzyme. This package doesn’t have it’s own assertion library. So, we can use assertion library provided by Jest.

We can test following things with the help of Jest and Enzyme:

  • We can test state changes in the components.
  • We can test conditional parameters passed in the component. E.g. Suppose the className of div tag is calculated at run-time based on the received props, we can test it.
  • We can test event handling in the component.
  • We can test component life cycle callbacks. Here we can test whether desired function is called from that life cycle hook or not.

Here are my observations about testing React – Redux application with Jest:

  1. Jest provides a very good assertion and mocking library. We can test asynchronous code with the help of it. If you are new to testing ReactJs application, Jest will be the best choice. Due to parallel testing, it is a great choice for large projects.
  2. We cannot mock http fetch calls with Jest. We can use package like ‘jest-fetch-mock’ for it.
  3. We cannot mock Redux store with Jest. We can use packages like ‘redux-mock-store‘ to create mock store and ‘redux-thunk’ to provide middleware for the store.
  4. Snapshot testing is one of the best features provided by Jest. It is useful to check whether UI is changed unexpectedly or not.
  5. Snapshot testing creates a component tree. We are unable to traverse through this component tree using Jest only. We can use enzyme package along with Jest for it.

To sum it up, I think, Jest is really good framework for testing ReactJs part of the application. Using some packages like ‘redux-mock-store’, ‘redux-thunk’, ‘jest-fetch-mock’, ‘enzyme’ along with Jest, we can test entire React-Redux application.

Advertisements

Author: kiran deshmukh

I am working as a Software Engineer at Josh Software Pvt. Ltd. Currently, I am learning ReactJs.

2 thoughts on “Testing React-Redux App with Jest”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s