One of the beauties of React/Redux' modular architecture is how damn easy it is to test everything. Actions are simple functions that return an object given an input, reducers, are pure functions that have predictable results, and React has some awesome Test Utilities for running tests on DOM components.
In this post, I'm going to walk through how I set up some basic unit tests for my actions, reducers, and components for an example tic-tac-toe application built with React/Redux. If you want to check out the full application code that I'm referring to, checkout the full project on GitHub.
To write these tests, I used Mocha as my testing framework and expect as my assertion library. For testing the React components, I used Karma.
Set up For Action/Reducer Unit Tests
Our files and our tests are written in ES6. To test them easily, we can tell Mocha to run babel as a compiler. Our project directory already has a .babelrc
file which looks like this:
//.babelrc
{
"presets": ["es2015", "react"]
}
If you want some more info on babel and the .babelrc
file, checkout how to set up a React/ES6 build here.
We'll make use of a package called babel-register
which will hook itself into Node's require statement and will transform files on the fly. After installing babel-register
, we can set up our testing script inside of package.json
.
// package.json
...
"scripts": {
"test": "mocha 'test/mocha/**/*.js' --compilers js:babel-register --recursive"
},
...
The --compiler
flag will run babel-register allowing us to directly test our ES6 files.
Unit Testing Actions
Actions are one link in the Redux chain and because they are simple functions, they are really easy to test. In the example app, they just pass on the arguments they are given, so we will test for that behavior.
The action we'll test is markSquare
which will return an object with type 'MARK_SQUARE'
and a position object.
Here's the action code:
// action/index.js
export const markSquare = (pos) => {
return {
type: 'MARK_SQUARE',
pos
}
}
To test an action, we'll create an object with out expected outcome, expectedAction
, call the action function we're testing with the appropriate arguments, and then compare the actual response with our expectedAction
.
import expect from 'expect';
import * as actions from '../../src/action/index';
describe('Actions', () => {
it('should create a MARK_SQUARE action', () => {
const pos = {
x:0,
y:0
};
const expectedAction = {
type: 'MARK_SQUARE',
pos
};
expect(actions.markSquare(pos)).toEqual(expectedAction);
});
});
This example is trivial, but it's apparent how the structure of Redux is really built for ease of testing.
Testing Reducers
Assuming your building your doing your Redux right, your reducers are pure functions. Given the same inputs, they'll return the same result. Again, blissfully easy to test.
We will run tests on two essential features of our reducer. The first, is to check if it returns the correct initial state if passed an undefined
state argument.
The second test, will check that our board reducer responds correctly to a 'MARKSQUARE' action. Our 'MARKSQUARE' action will pass the board a coordinate pair and a mark to place at that location, ie 'X' at (0, 0).
There are more comprehensive tests as well as the reducer code up on the GitHub repo.
import expect from 'expect';
import reducer from '../../src/reducer/index';
describe('ticTacToe reducer', () => {
let state;
const initialBoard = [['', '', ''], ['', '', ''], ['', '', '']];
beforeEach(() => {
state = {
board: initialBoard,
winner: false,
players: ['X', 'O'],
turn: 0
};
});
it('should return initial state', () => {
expect(
reducer(undefined, {})
).toEqual(state);
});
it('should mark an empty square', () => {
let action = {
type: 'MARK_SQUARE',
pos: {
x: 0,
y: 0
}
}
let result = reducer(undefined, action)
expect(
result.board[0][0]
).toEqual(
'X'
);
});
});
Redux makes use of lovely pure functions to hold on to its logic, so writing unit tests for them is so easy it feels like cheating. Aside from a little babel/es6 set up, you can just import your actions and reducers and test away!
Testing React Components
Testing your components takes a little more overhead on the other hand, as they need to be rendered into the DOM. To do this, I opted to use Karma. Facebook has created the Jest utility for testing components, but I went with Karma mostly on the fact that Jest mocks everything except the component under test which could lead to some bugs getting missed. I also ran across issues concerning speed in Jest.
Karma Set Up
To set up Karma, we're going to integrate it with Webpack to handle our builds and ES6/Babel magic, and Mocha again as our test framework. This set up will handle testing in Chrome.
I got this set up from the awesome tutorial here.
To use Karma install the following npm packages:
npm i karma karma-cli karma-chrome-launcher
To get integrated, we'll need to install the following packages so Karma will play nice with Webpack and Mocha. In addition to our Webpack build packages, we'll need:
npm i karma-webpack karma-sourcemap-loader karma-mocha
We'll need to set up two additional files.
The first is our karma.conf.js
file which handles configuration for Karma.
var webpack = require('webpack');
var path = require('path');
var test_dir = path.resolve(__dirname, 'test/karma');
var app_dir = path.resolve(__dirname, 'src');
module.exports = function (config) {
config.set({
browsers: [ 'Chrome' ],
singleRun: true,
frameworks: ['mocha'],
files: [
'tests.webpack.js'
],
preprocessors: {
'tests.webpack.js': [ 'webpack', 'sourcemap' ]
},
reporters: [ 'dots' ],
webpack: {
devtool: 'inline-source-map',
module: {
loaders: [
{ test: /\.js$/, include:[test_dir, app_dir], loader: 'babel-loader'}
]
}
},
webpackServer: {
noInfo: true
}
});
};
The karma-webpack
package adds the preprocessors
and webpack
properties which we can pass to the Karma configuration function allowing us to dynamically compile our files under test.
As our files
value, we passed tests.webpack.js
. This file will do some Webpack magic to allow us to dynamically add any test files matching our passed regex expression. Check out more about dynamic contexts here.
var context = require.context('./test/karma', true, /-test\.js$/);
context.keys().forEach(context);
Double check your test files and component files are being included in karma.conf.js
and the regex from test.webpack.js
will match your test files. Trust me. Do it.
Component Tests
To test our components we'll make use of the React TestUtils library. TestUtils will handle rendering our components into the DOM, allowing us to grab and test DOM nodes within our components, and, best of all, simulate user interactions.
The first component we'll look at is GameStatus which displays the current turn and the winner if any. We'll feed in some mock props and check to make sure everything is rendered properly.
The first test is a sanity check to make sure that everything is wired up correctly.
The second test will set a winner through the props object. Our component should display the winner in an h2
tag, so we'll use TestUtils to find the node, get its text content and check it against an expected value.
import expect from 'expect';
import React from 'react';
import ReactDOM from 'react-dom';
import TestUtils from 'react-addons-test-utils';
// Component to Test
import GameStatus from '../../src/component/game_status';
import Grid from '../../src/component/grid';
describe('Components', () => {
describe('Game Status', () => {
const gameStatusProps = {
players: ['X', 'O'],
turn: 0,
winner: false
};
it('should render correctly', () => {
var gameStatus = TestUtils.renderIntoDocument(<GameStatus {...gameStatusProps} />);
expect(gameStatus).toExist();
});
it('should display the winner', () => {
gameStatusProps.winner = 'X';
var gameStatus = TestUtils.renderIntoDocument(<GameStatus {...gameStatusProps} />);
var h2 = TestUtils.findRenderedDOMComponentWithTag(gameStatus, 'h2');
expect(h2.textContent).toEqual('X Wins!');
});
});
});
TestUtil offers a Simulate
method which allows us to test user interactions. Simulate can mock out any user interaction React can deal with such as change
or click
. We will test our Grid
component to make sure that it's passing the appropriate arguments to our click handler.
First, we'll render our Grid
component into the DOM passing it some mocked props including a mocked out click handler function. Next, we'll grab a reference to the first <td>
node on the board, at (0, 0). Then we will use Simulate
to click it, and check the value received by the click handler function to our expected value.
describe('Grid', () => {
let arg;
const gridProps = {
board: [['',''], ['','']],
onSquareClick: function(val){
arg = val;
}
}
it('should return the x and y value of the clicked square', () => {
const grid = TestUtils.renderIntoDocument(<Grid {...gridProps} />);
const square = TestUtils.scryRenderedDOMComponentsWithTag(grid, 'td')[0];
TestUtils.Simulate.click(square);
expect(arg).toEqual({x:0, y:0});
});
});
The structure of React/Redux apps really push developers towards creating easily testable code. The pure functions at the heart of Redux, and the modularity underpinning React, really make writing unit tests a breeze with minimal testing overhead so you can focus on writing code, and not your tests.