React, Redux and WebSocket Unit Testing.

Ian Ovenden
Aug 22, 2017 · 18 min read

Unit testing front-end code is critical in modern web application development. This article details a test suite developed to provide coverage for React, Redux and WebSocket functionality.

The tests outlined here relate to a web application detailed in my recent articles on Redux Web Socket Integration and Adding Offline Support to Redux.


The importance of unit testing

An increase in continuous delivery pipelines for rapid product iteration, coupled with the uptake of development processes that start with the formalization of test scripts (Test Driven Development [TDD] and subsequently Behavior Driven Development [BDD]) has placed more importance on the ability to automatically test both discrete areas of functionality through unit testing, and application interactions through behavioral/functional testing.

No longer the domain of server side development, automated testing of front-end code is critical in modern web application development. With a continuous delivery pipeline deploying to production, it is vital to have confidence that the code is defect free.

The development practices of Test Driven Development (TDD) and Behavioral Driven Development (BDD) see test scripts written first, describing scenarios functional changes are required to meet. Once approved, code is written to satisfy these test specifications. Many developers favor this approach as it enables a more focused, clear presentation of logic through natural language that everyone on the project team can understand.

This article focuses on the development of a test suite developed to run a set of specification scripts for the previously referenced ToDo application. Multiple useful libraries are included in the suite and will be described here. We’ll also look at some example test scripts as we go.

Behavioral testing is out of the scope of the article.


Core Test Platform

There are many test platforms available to front-end developers today, along with multiple assertion, mocking, and utility libraries. These are often interchangeable and while it can be argued that some work better together, a number of combinations are possible. I don’t believe there is a “best set”, it’s dependent upon many factors, and a “best fit” for your application is a more desireable goal. It’s definitely worth trying out different combinations to find the right one for your application and requirements.

Given my previous experience using the Mocha test framework with the Chai assertion library, I researched the viability of using them together at the core of my React application test suite. Good news, they were deemed a good fit.

I did see a lot of commentary around using Jest over Mocha, which is maintained by the Facebook JavaScript Tools team. This is certainly something I’m going investigate further and possibly migrate too given it’s focus on React app testing and claimed performance gains.

Here’s a useful article from Gary Borton of the Airbnb engineering team on the benefits they experienced and steps to migrate.

Given my familiarity with Mocha, I decided to stick with it for now and come back to Jest later.


Installation and Configuration

The first step is to install our dependencies

yarn add mocha chai -D

Once installed, the test script needs to be setup and configured within the application's package.json file:

...
"scripts": {
...
"test:unit": "mocha --compilers js:babel-core/register test/unit --recursive"
}
...

As the code isn’t being compiled by Webpack (as the application source code is), we need to specify that the code run through the babel-core compiler. The script also needs to know the location of our test specs — test/unit. The additional --recursive option is also passed to the script given the sub-directories that make up our test spec folder structure:

Now in order to run our tests we simply run the following command:

$ npm run test:unit

With the base of the test suite in place I went on to look at writing test specifications for each React/Redux module type.

React / Redux tests scripts should be straightforward to write due to the nature of the application source code — you should be writing pure functions, meaning the output is highly predictable and therefore testable.


Reducers

The reducer unit tests check each Action Type works as expected. As a very simple example, let's look at our REQUEST_BOARDS action type in the boards reducer:

...
case REQUEST_BOARDS:
return Object.assign({}, state, {
isFetching: true
});
...

All this action type does, is update the isFetching property of our state to true, so we can test as follows:

import { expect } from 'chai';
import { REQUEST_BOARDS, RECEIVE_BOARDS } from '../../../src/constants/action-types';
import boards from '../../../src/reducers/boards';
describe( 'Board reducer', () => {
it( 'Can request a list of boards - REQUEST_BOARDS', () => {
let state = { isFetching: false };
expect(
boards( state, {type: REQUEST_BOARDS})
).to.eql({ isFetching: true });
});
...
});

The isFetching value of the initial state is passed in as “false”. When the type REQUEST_BOARDS action type is passed to the boards reducer, we should expect the isFetching value to be returned as “true”.

Action Creators

Testing action creators is a more complicated proposition. We can easily check that the correct action object types are returned. For example, if we look at the source code for a simple function that just returns a type:

export function requestBoards() {
return {
type: REQUEST_BOARDS
};
}

In this case REQUEST_BOARDS is set from a constants file. The same constant can be used in our test and we can ensure the type is as expected:

it( 'Will return an action object of type REQUEST_BOARDS', () => {
expect( boards.requestBoards().type ).to.eql( types.REQUEST_BOARDS );
});

Here is a slightly more complex example function which determines whether “boards” should be requested or not:

export function shouldFetchBoards( state ) {
const boards = state.boards;
if ( !boards ) {
return true;
} else if ( boards.isFetching ) {
return false;
} else {
return true;
}
}

To test this function the state is set as required to test each of the possible conditions:

it( 'will determine whether or not it is appropriate to fetch boards - shouldFetchBoards', () => {
let state = {
boards: {
isFetching: false,
items: {
name: 'boardX',
id: 1
}
}
};
expect( boards.shouldFetchBoards( [] ) ).to.eql( true );
expect( boards.shouldFetchBoards( state ) ).to.eql( true );
expect( boards.shouldFetchBoards( Object.assign( state, {boards: {isFetching: true}}) ) ).to.eql( false );
});
...

There are three assertions executed here; the first one sees an empty state passed to the function. As boards doesn’t exist it should return “true” — retrieve some boards. Secondly, the default state is passed in whereby isFetching is set to false. Again this returns “true” and boards should be requested.

Finally, the isFetching value of the state is set to “true” before being passed into the function. This time the function should return “false” — we don’t want to request boards again, as they are already being fetched.

Asynchronous Action Creators

The complexity increases further still when testing asynchronous action creators.

Redux by default doesn’t support asynchronous actions such as fetching data, but we can employ the Thunk middleware to provide support for returning functions within an action creator. These inner functions can receive the store methods dispatch and getState, providing the control needed.

To learn more about Redux and Thunk, I’d recommend this excellent article from Matt Stow:

Now our action creators can return functions, we can perform various tasks such as data retrieval. But how do we test these?

As an example, let’s look at the fetchBoards action creator:

export function fetchBoards() {
return dispatch => {
dispatch( requestBoards() );
return apiGetBoards()
.then( response => response.json() )
.then( json => dispatch( receiveBoards( json ) ) );
};
}

This function does a few things, so let’s break it down.

  1. The requestBoards() function is dispatched, which if you recall from earlier, sets isFetching:true on the boards slice of our state.
  2. apiGetBoards() is called which performs the actual fetch request:
export function apiGetBoards() {
return (
fetch( BOARDS_ENDPOINT )
);
}

3. The response is converted into json format.

4. The receiveBoards action creator is dispatched with the json containing our board data:

export function receiveBoards( json ) {
return {
type: RECEIVE_BOARDS,
boards: json,
receivedAt: Date.now()
};
}

We want to test that after fetchBoards() has been dispatched the state matches our expectations. In order to test our state we need a store — for this I used Redux Mock Store. We can use this store to dispatch our chosen creator(s), before calling getActions() to verify they match our expectations.

Include the dependency through yarn add redux-mock-store nock -D.

Before moving on to look at an example, there is another dependency we have to include — an HTTP mocking and expectations library for Node.js called Nock.

Continuing with our example from earlier, Nock is used to mock the fetchBoards() API fetch response:

nock( api.BOARDS_ENDPOINT )
.get( '' )
.reply( 200, {
isFetching: false,
items: {
name: 'boardX',
id: 1
}
});

A request to the BOARDS_ENDPOINT will be mocked to reply with a single board “boardX” that has an id of “1” and an “isFetching” status of false.

The test specification file imports and configures these modules, setting up a mock store that supports Thunk and a mocking library to fake our responses. This allows us to test our async action creators. The code below pulls these concepts together and illustrates how the fetchBoards() action creator can be tested.

import { fetch } from 'isomorphic-fetch';
import { expect } from 'chai';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import nock from 'nock';
import * as types from '../../../src/constants/action-types';
import * as api from '../../../src/constants/api';
import * as boards from '../../../src/actions/boards';
// configure the mock store
const middlewares = [ thunk ];
const mockStore = configureMockStore( middlewares );
describe( 'Board actions', () => { ... it( 'test the async action creators with a mock store.', () => { // mock up the board endpoint api response
nock( api.BOARDS_ENDPOINT )
.get( '' )
.reply( 200, {
isFetching: false,
items: {
name: 'boardX',
id: 1
}
});
// set expectations - what actions are we expecting to see
const expectedActions = [
{ type: types.REQUEST_BOARDS },
{ type: types.RECEIVE_BOARDS,
boards: {
isFetching: false,
items: {
name: 'boardX',
id: 1
}
},
receivedAt: Date.now()
}
];
const store = mockStore(); // dispatch the fetchBoards() action creator on the mock store
return store.dispatch( boards.fetchBoards() )
.then( () => { // return of async actions
// test the store actions to see if they meet our expectations
expect( store.getActions() ).to.eql( expectedActions );
});
});
});

In summary, this script sets up a mock store using Thunk middleware, mocks any data responses with Nock, sets up our expected actions object, dispatches the action creator against with the mock store and compares the actions logged with our expected actions.


Components

With test coverage provided for functionality relating to our state model let's move on to testing the output of our React components. We should validate our markup structure and CSS class assignments for example.

A very useful library to use in this context is Enzyme from Airbnb:

Enzyme is a JavaScript Testing utility for React that makes it easier to assert, manipulate, and traverse your React Components’ output.

Whilst an unopinionated library, the documentation and examples provided for Enzyme fortunately use Mocha and Chai which goes a little way towards validating the decision to use these in our core test platform.

Shallow v Full Rendering

Enzyme exposes two different ways to render components which it’s useful to understand before proceeding.

Shallow rendering constrains your testing to your chosen component — it ensures that your tests aren’t directly asserting on behaviour of child components. In most cases for my particular app, shallow rendering was all that was required. There were a couple of tests however, where I had to implement full rendering.

Full DOM rendering is required for cases where you need to test components which may interact with DOM API’s or that require the full lifecycle in order to test fully. This is where JSDom comes into play — allowing us to mount the component within a headerless browser environment for testing.

Test Harness

Generating the test specifications for React components required importing all required libraries and utility files and repeating code in each one. To avoid repetition I moved all the common imports and code blocks to a utility/harness.js file exposing a single harness function.

The test harness includes a lot of the functionality already discussed — Redux Mock Store, Thunk, Nock. To test the React components it was also necessary to include Enzyme and a utility library, JSDom. JSDom provides a JavaScript implementation of the DOM and HTML standards for use with Node.js — this provides the ability to render components within our browserless testing environment.

Here is the full source of the file:

import React from 'react';
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import { shallow, mount } from 'enzyme';
import nock from 'nock';
import jsdom from 'jsdom';
import * as types from '../../../src/constants/action-types';
// setup a dummy redux store with Thunk
const middlewares = [ thunk ];
const mockStore = configureStore( middlewares );
// create global document for enzyme mount()- for full DOM rendering
const doc=jsdom.jsdom('<!doctype html><html><body></body></html>');
global.document = doc;
global.window = doc.defaultView;
// harness function
export function harness( Component, getState, nockObj, presentational=false, renderType='shallow' ) {
let enzymeWrapper = {};
let options = {};
// Get our store
const store = mockStore( getState );
// Set our props
const props = {
store: store
};
// if required, mock up an endpoint
if ( nockObj.endpoint ) {
nock( nockObj.endpoint )
.get( nockObj.get )
.reply( 200,
nockObj.reply
);
}
// enforce the store if component isn't presentational
if (presentational ){
options = {
context: { store }
}
} else {
options = {
context: { store },
childContextTypes: {store: React.PropTypes.object.isRequired }
};
}
// determine the Enzyme function to use
if ( renderType === 'shallow' ){
enzymeWrapper = shallow( Component );
} else {
enzymeWrapper = mount( Component, options );
}
return {
props,
enzymeWrapper
};
}

This file contains imports for all dependencies and sets up a number of things for us. It creates a mock store and a global document object model, allows for a simulated endpoint response to be configured through Nock, enforces store validation checks for non-presentational components before finally using Enzyme to wrap the passed component with the appropriate wrapper. This is then returned along with any props.

Component specifications

With a harness in place, let’s move on to look at how we can utilise this within the test specs for our React components. The example below goes through each step required to generate a test specification for the<BoardList/> component.

Firstly, we’ll need to import React, our assertion library, the component file, the API constants and our harness function:

import React from 'react';
import { expect } from 'chai';
import BoardListContainer from '../../../src/containers/board-list';
import * as api from '../../../src/constants/api';
import { harness } from '../.utility/harness';

This is followed by declarations, including the render type and our nockObj object which contains all required data for our mock endpoint configuration:

// Use full render
const renderType = 'mount';
// data required for nock endpoint simulation
const nockObj = {
endpoint: api.BOARDS_ENDPOINT,
get: '',
reply: {
isFetching: false,
items: {
name: 'boardX',
id: 1
}
}
};
let initialState = {};

As the BoardList component may contain child components that affect its rendered output, we need to employ the use of the full render API and mount the component to our global document object model.

In the first test we verify that the component renders itself and it’s children (as specified in the state object and the nock data) correctly:

it( 'BoardListContainer: should render self and subcomponents', () => {
initialState = {
boards: {
isFetching: false,
items: [{
name: 'boardX',
id: 1
}]
}
};
const { enzymeWrapper } = harness( <BoardListContainer />, initialState, nockObj, true, renderType ); const enzymeHTML = enzymeWrapper.html();
const BoardList = enzymeWrapper.find( 'BoardList' );
// validate the HTML structure
expect( enzymeHTML ).to.eql( '<nav class="boardlist"><ul style="opacity: 1;"><li><a>boardX</a></li></ul></nav>' );
});

The BoardListContainer is passed into our test harness and an enzymeWrapper returned. The HTML can then be extracted to test enzymeWrapper.html(). In the above example, we are comparing the rendered output to

<nav class="boardlist"><ul style="opacity: 1;"><li><a>boardX</a></li></ul></nav>

But how did we arrive at this HTML? Lets have a look at the render function for the BoardListContainer:

render() {
const { boards, isFetching, lastUpdated } = this.props;

return (
<nav className="boardlist">
{isFetching && boards.length === 0 &&
<h2>Loading...</h2>
}
{!isFetching && boards.length === 0 &&
<h2>Empty.</h2>
}
{boards.length > 0 &&
<BoardList boards={boards} />
}
</nav>
);
}

The rendered output contains a <nav> element with the className attribute of “boardlist”. The conditional statements within this element determine the child elements that are rendered. If there are boards to be displayed here, the <BoardList/> child element is rendered (hence why we need to use the full render API).

This component is presentational and returns an unordered list with a list item for every board:

render() {
return (
<ul style={{ opacity: this.props.boards.isFetching ? 0.5 : 1 }}>
{this.props.boards.map( ( board ) =>
<li key={board.id}><Link to={{ pathname: '/board/' + board.id } }>{board.name}</Link></li>
)}
</ul>
);
}

To conclude this test specification, two further tests check that the rendered output is correct for when we have no boards but are currently fetching them and for when we have a response back which contains no boards — empty:

it( 'BoardListContainer: should render empty when no boards are available', () => {
initialState = {
boards: {
isFetching: false,
items: []
}
};
const { enzymeWrapper } = harness( <BoardListContainer />, initialState, nockObj, true, renderType ); const enzymeHTML = enzymeWrapper.html(); // validate the HTML structure
expect( enzymeHTML ).to.eql( '<nav class="boardlist"><h2>Empty.</h2></nav>' );
});
it( 'BoardListContainer: should render loading animation when fetching', () => {
initialState = {
boards: {
isFetching: true,
items: []
}
};
const { enzymeWrapper } = harness( <BoardListContainer />, initialState, nockObj, true, renderType ); const enzymeHTML = enzymeWrapper.html(); // validate the HTML structure
expect( enzymeHTML ).to.eql( '<nav class="boardlist"><h2>Loading...</h2></nav>' );
});

It should be noted that since the context is being set through passing in the state, the nock object is somewhat redundant. However since part of the render process involves making a request, if it isn’t mocked a warning is thrown. Mocking the response keeps the tests clean.


WebSockets

When WebSocket functionality for bi-directional communication was integrated into the application, the test coverage had to expand accordingly.

This required a way to test messages being emitted from action creators and validate dispatches triggered when receiving messages.

Through investigation I came upon another very useful mocking library, Mock Socket which mocks connections for WebSockets and Socket.io. It turns out this library is a perfect fit with the mock store already in place within our test suite. Here are the steps to get it up and running.

Firstly, we need to install the library as a dependency:

yarn add mock-socket --dev

To handle the setup and configuration of Websockets, I created a websockets.js utility file which very closely matches the WebSocket setup source file. Please refer to previous article:

Here is the code for the mock socket utility file:

import { SocketIO as socketIO, Server } from 'mock-socket';
import {messageTypes, uri} from '../../../src/constants/websocket.js';
// SERVER
export const mockServer = new Server( uri );
let messages = [];/*
This step is very important! It tells our chat app to use the mocked
websocket object instead of the native one. The great thing
about this is that our actual code did not need to change and
thus is agnostic to how we test it.
*/
window.io = socketIO;const socket = socketIO( uri );export const init = ( store ) => { // add listeners to supported socket messages so we can re-dispatch them as actions Object.keys( messageTypes ) .forEach( type => socket.on( type, ( payload ) => store.dispatch({ type, payload }) ) );
};
export const emit = ( type, payload ) => {
messages.push( type, payload );
socket.emit( type, payload );
};
export const getMessages = () => {
return messages;
};

The module sets up the mock socket based on our application settings, before informing the app to use the mock socket rather than the native one. The exported init() function sets up a socket for each supported message type and will be called on our mock store.

The emit() function emits the message on the mock socket and also adds it to a messages array so we can keep track of messages being sent. We can later retrieve these messages through the getMessages() function.

How this new functionality is used can be seen in the test for updating a stage name. Not only should the name update, a message should be emitted to other users in order to sync up the state of their apps.

The WebSocket dependencies are imported into our test specification file and used when creating our mock store:

import { expect } from 'chai';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import nock from 'nock';
import * as types from '../../../src/constants/action-types';
import * as api from '../../../src/constants/api';
import * as stages from '../../../src/actions/stages';
import { init as websocketInit, emit, getMessages, mockServer } from '../.utility/websocket';
const middlewares = [ thunk.withExtraArgument({ emit }) ];
const mockStore = configureMockStore( middlewares );

The emit() function is added through the thunk middleware, making it available to our action creators through the mock store.

With this in place, lets look at how we test the WebSocket functionality within our specific stage name update test:

it( 'should update the stage name AND broadcast a message to the specified web socket', () => {
const store = mockStore();
// setup our required websockets on the mock store.
websocketInit( store );
// set our expected actions
const expectedActions = [
{ type: types.UPDATE_STAGE_TITLE,
'meta': {
'offline': {
'commit': {
'meta': {
'stageId': 1,
'stageTitle': 'stageY'
},
'type': 'UPDATE_STAGE_TITLE'
},
'effect': {
'method': 'GET',
'url': ''
}
}
},
payload: {
stageId: 1,
stageTitle: 'stageY'
}
}
];
// set our expected emit messages
const expectedEmit = [
types.UPDATE_STAGE_TITLE,
{
stageId: 1,
stageTitle: 'stageY'
}
];
// dispatch the action creator we are testing on the mock store
store.dispatch( stages.wsStageTitle( 1, 'stageY' ) );
// check to see that the actions match
expect( store.getActions() ).to.eql( expectedActions );
// check web socket comms SEND - do messages sent match expected?
expect( getMessages() ).to.eql( expectedEmit );
mockServer.emit(
types.UPDATE_STAGE_TITLE,
{
stageId: 1,
stageTitle: 'stageWS'
});
//check web socket comms RECEIVE
expect( store.getActions()[1] ).to.eql(
{
type: 'UPDATE_STAGE_TITLE',
payload: { stageId: 1, stageTitle: 'stageWS' } }
);
});

We are essentially testing two things here, the actions being dispatched against the store and the messages being emitted over WebSockets.

One of the first things we do in this test is initialise the WebSockets on the mock store:

websocketInit( store )

To test messages are being SENT correctly, we simply check the message array set up in our utility file. Messages are added to this when emitted on a socket:

expect( getMessages() ).to.eql( expectedEmit );

Further to this, we also want to ensure that when a message is RECEIVED, the correct action is dispatched. To do that, we call emit() on the mock socket server with a given payload. We then verify that the mock store has the dispatched action listed with the correct payload data.

mockServer.emit(
types.UPDATE_STAGE_TITLE,
{
stageId: 1,
stageTitle: 'stageWS'
});
//check web socket comms RECEIVE
expect( store.getActions()[1] ).to.eql(
{
type: 'UPDATE_STAGE_TITLE',
payload: { stageId: 1, stageTitle: 'stageWS' } }
);
});

The tight integration with the Redux Mock Store makes testing WebSocket functionality with Mock Socket fairly straightforward and is a great addition to our test suite.


Date Stamp Issues

One issue I have encountered through the formation of this suite concerns date stamp comparisons. For example, when looking at the action creator for fetching boards, the expected actions data equates to:

//what actions are we expected to see called.const expectedActions = [
{ type: types.REQUEST_BOARDS },
{ type: types.RECEIVE_BOARDS,
boards: {
isFetching: false,
items: {
name: 'boardX',
id: 1
}
},
receivedAt: Date.now()
}
];

The “receivedAt” date is explicitly set to Date.now(). However, although the fetchBoards() action creator is dispatched almost immediately after the expectedActions declaration, there is always going to be a very minor delay which would lead to a failing assertion.

I couldn’t find a way to avoid this problem other than to explicitly set the date stamps to the same value. In the below code you can see a check to confirm the values are within a 1 second tolerance of each other, before the expected action “receivedAt” date stamp is set to match that of the actual action. Therefore, when the expected actions and store actions are compared, the dates will always match.

return store.dispatch( boards.fetchBoards() )
.then( () => { // return of async actions
// ensure receivedAt time stamps are within an appropriate tolerance level.
expect( store.getActions()[1].receivedAt ).to.be.closeTo( expectedActions[1].receivedAt, 1000 ); // impossible to accurately compare receivedAt values, since there would be a delay in receiving the reply. // deliberately match the values so as to pass the test.
expectedActions[1].receivedAt = store.getActions()[1].receivedAt;
expect( store.getActions() ).to.eql( expectedActions );});

This doesn’t have any detrimental impact on the integrity of our tests, but it does feel a little like a hack. Certainly worth being aware of.


Conclusion

Tests are a vital component of modern front-end development providing benefits to development procedures, code robustness and deployment practices.

With a solid test suite in place, unit tests needn’t add much overhead to functional development and can provide clarity to wordy specification documents.

There are many ways to configure a test suite; this is just one example. The libraries available certainly help achieve a high coverage of application code and I’d certainly recommend their evaluation when constructing your own suite. The platform detailed here provides a testing solution for all facets of my app, which was my goal after all.

That isn’t to say this is a definitive guide — your suite should meet your needs. It should also evolve over time to meet any new requirements.

)
Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade