Codementor Events

Unit Testing a Redux App

Published Dec 07, 2015Last updated Aug 10, 2017
Unit Testing a Redux App

At Codementor, we use React to build some of our services. Here’s the second part of the React & Redux series written by one of our devs!


For the past few months, I’ve been using React & Redux for a few projects, and I’d like to write about my experience with Redux in this 3-part series:

  • Hello Redux: This article will introduce you to Redux and go through the reasons I think it is awesome
  • Server-Side Rendering: This will be a tutorial on how to use Redux and react-router to do server-side rendering
  • Unit Testing : I’ll talk about the problems I faced when trying to test Redux code and how I solved them. I’ll also talk about how to make sure your webpack loaders won’t interfere with your tests.

In this part, I’ll be going over unit testing.

On Unit Testing

Unit testing is a big subject to cover: from why you need to unit test, to setting up your testing environment, to designing a testable architecture for your codebase, etc. In most cases, the way of testing would largely depend on what libraries or frameworks are used in your production code. For example, an app built with React + Redux would be tested much differently than an app that was built with AngularJS.

In this section I’ll go over a few basic concepts in unit testing, and after that I’ll focus on React & Redux related issues.

What is Unit Testing and How is it Different than Other Forms of Testing?

Unit testing is a type of automated test that is the closest to the code itself, as the target of the unit test is often a class, function, React component, etc – things you can split up with code. In other words, unit tests are written for developers.

Compared with a higher-level acceptance testing, the acceptance test code itself is a bit farther away from your production code, as acceptances test targets features (e.g. was the signup successful, can you create a user through some button, etc). Basically, acceptance tests are written for those who are all talk can’t code (e.g. Project Managers XD).

For example, a unit test’s purpose could be as follows:

  • to make sure a questionReducer will return a new question as a state after receiving the QUESTION_LOADED event.

Alternatively, an acceptance test would aim for something as follows:

  • to make sure when a user presses a question link, the user will be brought to the question page where all questions have been rendered.

Why Do We Need to Write Unit Tests?

Unit tests will help us make sure our code behaves as expected

As a code base gets bigger and gets more collaborators, it becomes impractical to manually check if every function or class behaves as expected. If we had automated tests to check if things are working as expected every time we change our code, this will greatly reduce the time we’d spend on debugging. In other words, unit tests will allow every collaborator to make bold changes to the code, because the tests will easily let them know whether or not they accidentally broke something.

With unit tests, developers can refactor their code without having to worry about breaking it, and in time this becomes a good cycle that will make the code stabler. What’s more, unit tests will help reduce the development time of adding new features or making changes to the code. Altogether, unit tests come with many awesome benefits.

In addition to being code stabilizers, you can also view unit tests as documentations that won’t lie.

I believe most of us have the experience of coming back to a code 2 weeks later and wondering who the hell wrote this horrid mess, only to use git blame to find out that you were the one who wrote it.

If you’re a forgetful person, unit tests can play the role of a reminder. That is, if you forgot why a function was coded or how to use it, you can take a look at the testing code as it would give you a demo.

Enough. What About Redux?

When adding tests to a Redux app, you can break the steps down in the following manner:

  1. Choose a testing framework as well as an assertion and mocking library (e.g. mocha, jasmine, etc.) Configure the settings.

  2. Start writing the tests, which can be broken down into:

    • Action Tests
    • Reducer Tests
    • Middleware Tests
    • Component Tests
  3. Figure out how to deal with the webpack

In this article, I will skip the first step, since you can find the configuration details here. The following guide also assumes that readers have a basic understanding of how mocha and chai APIs work.

Time to Write Some Tests!

…Not XD

Before we get to writing the tests, we need to break down the process into several steps:

  • Define what subject needs to be tested
  • Define what behavior (of the subject) needs to be tested
  • setup the testing environment that will execute the behavior we want to test
  • verify that the result is as expected

I won’t be bringing in all of my code, so feel free to refer the source code here if needed.

The Action Test

Under the Redux design, actions are relatively simple. On the surface, an action just returns an action object.

For example, if we have an actions/questions.js, it has a loadQuestion function that we’ll be looking to test:

import { CALL_API } from 'middleware/api'; 

export const LOADED_QUESTIONS = 'LOADED_QUESTIONS'; 
export function loadQuestions() { 
  return { 
    [CALL_API]: { 
      method: 'get', 
      url: 'http://localhost:3000/questions', 
      successType: LOADED_QUESTIONS 
    } 
  }; 
}

So, based on the aforementioned steps, we know the following:

  • what needs to be tested: question action creator

  • what behavior needs to be tested

    • loadQuestions() will return an object that contains the CALL_API key, and it should contain data we expect

Our action test would thus look like this in spec/actions/questions.test.js

import { CALL_API } from 'middleware/api';

// setup
import * as actionCreator from 'actions/questions';
import * as ActionType from 'actions/questions';

describe('Action::Question', function(){
  describe('#loadQuestions()', function(){
    it('returns action CALL_API info', function(){
      // execute
      let action = actionCreator.loadQuestions();

      // verify
      expect(action[CALL_API]).to.deep.equal({
        method: 'get',
        url: 'http://localhost:3000/questions',
        successType: ActionType.LOADED_QUESTIONS
      });
    });
  });
});

The Reducer Test

In Redux, a reducer acts like a function that connects a state to an action, and then returns a new state. In other words, a reducer will receive an action, and then decide how the state should be changed based on the action and the current state.

For example, let’s say we have a reducer reducer/questions.js:

import * as ActionType from 'actions/questions';

function questionsReducer (state = [], action) {
  switch(action.type) {
    case ActionType.LOADED_QUESTIONS:
      return action.response;
      break;
    default:
      return state;
  }
}

export default questionsReducer;

This time, we’ve defined the steps as:

  • What needs to be tested: question reducer

  • What behavior needs to be tested:

    • when questionsReducer receives the LOADED_QUESTIONS action, it will set action.response as the new state
    • when met with an action type it does not recognize, questionsReducer will return a blank array.

And thus, our test for a reducer would look like this at spec/reducers/questions.test.js:

import questionReducer from 'reducers/questions';
import * as ActionType from 'actions/questions';

describe('Reducer::Question', function(){
  it('returns an empty array as default state', function(){
    // setup
    let action = { type: 'unknown' };

    // execute
    let newState = questionReducer(undefined, { type: 'unknown' });

    // verify
    expect(newState).to.deep.equal([]);
  });

  describe('on LOADED_QUESTIONS', function(){
    it('returns the <code>response</code> in given action', function(){
      // setup
      let action = {
        type: ActionType.LOADED_QUESTIONS,
        response: { responseKey: 'responseVal' }
      };

      // execute
      let newState = questionReducer(undefined, action);

      // verify
      expect(newState).to.deep.equal(action.response);
    });
  });
});

The Middleware Test

In a Redux app, the middleware is responsible for intercepting an action that was dispatched to a reducer and changing the action’s original behavior before it reaches the reducer. The middleware itself is a function with a signature that looks like this:

function(store) {
  return function(next) {
    return function(action) {
      // middleware behavior...
    };
  };
}

If you use ES6 syntax, this will look cleaner, though its nature is just as complex:

store => next => action => {
  // middleware behavior...
}

Anyhow, I think this is one of the most elegant features in Redux. I’ll explain why in detail in the next part, but for now let’s first take care of the test.

Let’s say we have an API middleware middleware/api.js

import { camelizeKeys } from 'humps';
import superAgent from 'superagent';
import Promise from 'bluebird';
import _ from 'lodash';

export const CALL_API = Symbol('CALL_API');

export default store => next => action => {
  if ( ! action[CALL_API] ) {
    return next(action);
  }
  let request = action[CALL_API];
  let { getState } = store;
  let deferred = Promise.defer();
  let { method, url, successType } = request;
  superAgent[method](url)
    .end((err, res)=> {
      if ( !err ) {
        next({
          type: successType,
          response: res.body
        });
      }
      deferred.resolve();
    });

  return deferred.promise;
};

Basically, the middleware does the following:

  • intercepts an action with a CALL_API key
  • based on the url in the CALL_API‘s value (let’s say it’s called request), method will send an HTTP API call request to the server
  • once the API is called successfully, the middleware dispatches a request.successType action
  • the middleware itself will return a promise. This promise will be resolved after the API has been called successfully. (A more complete version of what happens is that it should have a respective error handling, but to keep things simple we will skip this part).

So, implementing the steps above, we can define the test as follows:

  • What needs to be tested: api middleware
  • What behavior needs to be tested:
    • the middleware will ignore actions without a CALL_API key
    • the middleware will send the server an API call based on action[CALL_API]
    • the middleware will dispatch an action[CALL_API].successType event after a successful request
    • the middleware will resolve the middleware return promise after a successful request

As such, our test code spec/middleware/api.test.js would look as follows:

import nock from 'nock';
import apiMiddleware, { CALL_API } from 'middleware/api';

describe('Middleware::Api', function(){
  let store, next;
  let action;
  let successType = 'ON_SUCCESS';
  let url = 'http://the-url/path';

  beforeEach(function(){
    store = {};
    next = sinon.stub();
    action = {
      [CALL_API]: {
        method: 'get',
        url,
        successType
      }
    };
  });

  describe('when action is without CALL_API', function(){
    it('passes the action to next middleware', function(){
      action = { type: 'not-CALL_API' };
      apiMiddleware(store)(next)(action);
      expect(next).to.have.been.calledWith(action);
    });
  });

  describe('when action is with CALL_API', function(){
    let nockScope;
    beforeEach(function(){
      nockScope = nock(http://the-url)
                    .get('/path');
    });
    afterEach(function(){
      nock.cleanAll();
    });
    it('sends request to path with query and body', function(){
      nockScope = nockScope.reply(200, { status: 'ok' });

      apiMiddleware(store)(next)(action);

      nockScope.done();
    });

    it('resolves returned promise when response when success', function(){
      nockScope = nockScope.reply(200, { status: 'ok' });

      let promise = apiMiddleware(store)(next)(action);

      return expect(promise).to.be.fulfilled;
    });
    it('dispatch successType with response when success', function(done){
      nockScope = nockScope.reply(200, { status: 'ok' });
      let promise = apiMiddleware(store)(next)(action);

      promise.then(()=> {
        expect(next).to.have.been.calledWith({
          type: successType,
          response: { status: 'ok' }
        });
        done();
      });
    });
  });
});

This test is more complex than the previous ones, as nock is a library used to test the HTTP requests on Node.js. Nock is out of this article’s scope, so let’s just assume you’re familiar with Nock.js XD (if not, feel free to check out this Node.js tutorial on testing HTTP Requests with Nock.js).

Anyhow, other than nock, let’s explain the code above in more detail:

First of all, we nest every beforeEach within describe, since this lets every describe have its own context. In addition, we can also make use of the local variable inside a describe function so the test inside the same describe can share the variable. (For example, nockScope only had access to the code inside the when action is with CALL_API describe block).

Secondly, we need to understand how to execute the middleware in a testing environment.

Since the middleware itself is a function, albeit one that is wrapped in store and next. To test it, we just need to invoke the wrapped function with mocked store and next one after another.

apiMiddleware(store)(next)(action);

Finally, the dispatch successType with response when success has asynchronous behavior. By default, mocha will not wait for an asynchronous code to finish executing. This means that before the asynchronous callback is executed, the mocha test case (it()) will have already ended, and therefore we would not be able to test some behaviors that happen after the asynchronous code is executed. We can easily solve this problem by adding a done argument to the function after it, and mocha will wait until the done is executed before it ends the test case. This way, we can call done after an asynchronous callback has been executed to make sure mocha will end the test case at the point we expect it to end.

The Component Test

In Redux, we separate React components into two types: a smart component, and a stupid dumb component. A Smart component refers to the component that is connected to Redux, while a dumb component is one that is exclusively dictated by props.

Technically, dumb component tests are not directly related to Redux, as you can consider them standard React components. Component testing usually have something to do with mocking, and I’ll only go through the basics in this section.

On the other hand, smart components are mostly similar with dumb components, but with an added connect behavior in which it will:

  1. inject as a prop the keys needed by a component from store.getState(), in which the keys are selected using the function mapStateToProps.

  2. inject parts of an action into a component as a prop

According to Redux’s official documentation, the recommended way to test smart components is to work around connect and directly test the component part.

For example, let’s say we have a component containers/Question.js:

import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { loadQuestions } from 'actions/questions';
import { Link } from 'react-router';
import _ from 'lodash';

class Question extends Component {
  static fetchData({ store }) {
    return store.dispatch(loadQuestions());
  }

  componentDidMount() {
    this.props.loadQuestions();
  }
  render() {
    return (
      <p>
        <h2>Question</h2>

        {
          _.map(this.props.questions, (q)=> {
            return (
              <p key={q.id}> { q.content }</p>

            );
          })
        }

        <Link to="/">Back to Home</Link>

      </p>

    );
  }
}

function mapStateToProps (state) {
  return { questions: state.questions };
}

export { Question };
export default connect(mapStateToProps, { loadQuestions })(Question);

Here we used the react-addons-test-utils to query and verify a component’s content.

Let me explain the part where a component renders a link back to /:

Since our testing target is Question Component, we are ignoring how other components work. We only care about the relationship between the Question component and other components. Taking Question as an example, we don’t want to directly render another component (Link) because problems may arise from trying to render other components. In those cases, it will be difficult for us to determine whether it’s the Question‘s problem or another component’s problem.

Nonetheless, we usually render other components within a component, so it’s quite common for us to run into problems because of this. One solution is to mock out other components from the test with __Rewire__.

Link = React.createClass({
  render() {
    return (<p>MOCK COMPONENT CLASS</p>)

  }
});
Container.__Rewire__('Link', Link);

This way, when Question gets rendered in the test, the Link we see will be a dummy component and not the real Link component. Consequently, we can go ahead and test the UI relationship between the Question component and the Link component.

let doc = TestUtils.renderIntoDocument(<Question {...props} />);
let link = TestUtils.findRenderedComponentWithType(doc, Link);

expect(link).not.to.be.undefined;
expect(link.props.to).to.equal('/');

How to Deal with Your Webpack

Webpack has a lot of magical loaders that will let us require many different types of JavaScript objects such as images, style sheets, etc. When picking loaders, we need to weigh the trade-offs. The pro a loader is that it packages everything we need together, and the con is that you can only run the loader’s code within your webpack.

If our app only needs to be executed on the client-side, loaders shouldn’t be a problem. However, if the same code needs to run on the server side for universal rendering, then this means the loader you chose would have to package the server-side code separately. In short, you’d need two webpack config files for a universal app.

Personally, I prefer to avoid loaders that would require a server-side and client-side configuration file if I need to do universal rendering. It is simpler to have the server run code on its own rather than through a webpack.

The example below is code that only needs to be executed on the client-side (it’s not used in this example), in which I’ve used the url loader to require images.

let SignupModal = React.createClass({
  render() {
    let cmLogo = require('Icons/white/icon_codementor.png');
    
    ...
  }
})

As you can see, when this component renders, it will require an image and will then proceed to happily break when it is tested under the nodejs environment.

One way to solve this is to wrap a function around the required image. This way, we can mock the function in our testing environment.

And thus, our component should now look like this:

import requireImage from 'lib/requireImage';

let SignupModal = React.createClass({
  render() {
    let cmLogo = requireImage('Icons/white/icon_codementor.png');
    
    ...
  }
})

In which requireImage is just a simple require:

/lib/requireImage.js

export default function(path) {
  return require(path);
}

This way, we can mock out the requireImage in our test:

describe('Component SignupModal', function(){
  let requireImage;
  beforeEach(function() {
    requireImage = function() {}
    SignupModal.__Rewire__('requireImage', requireImage);
  });
  
  it('can be rendered', function() {
    // now we can render here
  });
});

Conclusion

Testing takes effort and practice, but we can better understand how our code works through the process of writing tests. Most of the time, code that is easy to test is code that has been structured well.

As a project grows in size and collaborators, eventually bugs will start to overwhelm development if there are no tests in place. In addition, once we get more familiar with writing tests, the time that it takes to write tests will take far less than the time it takes to debug.

Redux is designed to make it easy to write tests, and I think it’s one of the aspects that make Redux so fine.

If you liked what you learned about testing… what are you waiting for?


This article was originally published in Chinese here by Yang-Hsing Lin, and was translated by Yi-Jirr Chen. Feel free to leave a comment below if you have any feedback or questions!

Discover and read more posts from Yang-Hsing
get started
post commentsBe the first to share your opinion
Riccardo Bartoli
8 years ago

Great article! One question: in your middleware test you’re using sinon but there is no import for it. Am I missing something? Thanks.

Jonathon Hibbard
8 years ago

One question I have is - where does __REWIRE__ come from? Is that part of an installed package?

Yang-Hsing Lin
8 years ago

It’s from babel-plugin-rewire: https://github.com/speedska…

Jonathon Hibbard
8 years ago

Thanks! that was the missing piece for me.

Show more replies