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 newquestion
as a state after receiving theQUESTION_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:
-
Choose a testing framework as well as an assertion and mocking library (e.g.
mocha
,jasmine
, etc.) Configure the settings. -
Start writing the tests, which can be broken down into:
- Action Tests
- Reducer Tests
- Middleware Tests
- Component Tests
-
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 testverify
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 theCALL_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 theLOADED_QUESTIONS
action, it will setaction.response
as the new state - when met with an action type it does not recognize,
questionsReducer
will return a blank array.
- when
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 theCALL_API
‘s value (let’s say it’s calledrequest
),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
anaction[CALL_API].successType
event after a successful request - the middleware will resolve the middleware return promise after a successful request
- the middleware will ignore actions without a
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:
-
inject as a
prop
the keys needed by a component fromstore.getState()
, in which the keys are selected using the functionmapStateToProps
. -
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!
Great article! One question: in your middleware test you’re using sinon but there is no import for it. Am I missing something? Thanks.
One question I have is - where does __REWIRE__ come from? Is that part of an installed package?
It’s from
babel-plugin-rewire
: https://github.com/speedska…Thanks! that was the missing piece for me.