Unit Testing

A robust test suite is a vital constituent of quality software. With a good test suite at his or her disposal, a developer can more confidently refactor or add features to an application. Test suites are an upfront investment that pay dividends over the lifetime of a system.

Testing user interfaces is notoriously difficult. Thankfully, testing React components is not. With the right tools and the right methodology, the interface for your web app can be just as fortified with tests as every other part of the system.

We'll begin by writing a small test suite without using any testing libraries. After getting a feel for a test suite's essence, we'll introduce the Jest testing framework to alleviate a lot of boilerplate and easily allow our tests to be much more expressive.

While using Jest, we'll see how we can organize our test suite in a behavior-driven style. Once we're comfortable with the basics, we'll take a look at how to approach testing React components in particular. We'll introduce Enzyme, a library for working with React components in a testing environment.

Finally, in the last section of this chapter, we work with a more complex React component that sits inside of a larger app. We use the concept of a mock to isolate the API-driven component we are testing.

Writing tests without a framework

If you're already familiar with JavaScript testing, you can skip ahead to the next section.

However, you might still find this section to be a useful reflection on what testing frameworks are doing behind the scenes.

The projects for this chapter are located inside of the folder testing that was provided with this book's code download.

We'll start in the basics folder:


$ cd testing/basics

The structure of this project:


$ ls
Modash.js
Modash.test.js
complete/
package.json

Inside of complete/ you'll find files corresponding to each iteration of Modash.test.js as well as the completed version of Modash.js.

We'll be using babel-node to run our test suite from the command-line. babel-node is included in this folder's package.json. Go ahead and install the packages in package.json now:


$ npm install

In order to write tests, we need to have a library to test. Let's write a little utility library that we can test.

Preparing Modash

We'll write a small library in Modash.js. Modash will have some methods that might prove useful when working with JavaScript strings. We'll write the following three methods. Each returns a string:

truncate(string, length)

Truncates string if it's longer than the supplied length. If the string is truncated, it will end with ...:


const s = 'All code and no tests makes Jack a precarious boy.';
Modash.truncate(s, 21);
  // => 'All code and no tests...'
Modash.truncate(s, 100);
  // => 'All code and no tests makes Jack a precarious boy.'

capitalize(string)

Capitalizes the first letter of string and lower cases the rest:


const s = 'stability was practically ASSURED.';
Modash.capitalize(s);
  // => 'Stability was practically assured.'

camelCase(string)

Takes a string of words delimited by spaces, dashes, or underscores and returns a camel-cased representation:


let s = 'started at';
Modash.camelCase(s);
  // => 'startedAt'
s = 'started_at';
Modash.camelCase(s);
  // => 'startedAt'

The name "Modash" is a play on the popular JavaScript utility library Lodash.

We'll write Modash as an ES6 module. For more details on how this works with Babel, see the aside "ES6: Import/export with Babel." If you need a refresher on ES6 modules, refer to the previous chapter "Using Webpack with create-react-app.".

Open up Modash.js now. We'll write our library's three functions then export our interface at the bottom of this file.

First, we'll write the function for truncate(). There are many ways to do this. Here's one approach:


function truncate(string, length) {
  if (string.length > length) {
    return string.slice(0, length) + '...';
  } else {
    return string;
  }
}

Next, here's the implementation for capitalize():


function capitalize(string) {
  return (
    string.charAt(0).toUpperCase() + string.slice(1).toLowerCase()
  );
}

Finally, we'll write camelCase(). This one's slightly trickier. Again, there are multiple ways to implement this but here's the strategy that follows:

  1. Use split to get an array of the words in the string. Spaces, dashes, and underscores will be considered delimiters.
  2. Create a new array. The first entry of this array will be the lower-cased version of the first word. The rest of the entries will be the capitalized version of each subsequent word.
  3. Join that array with join.

That looks like this:


function camelCase(string) {
  const words = string.split(/[\s|\-|_]+/);
  return [
    words[0].toLowerCase(),
    ...words.slice(1).map((w) => capitalize(w)),
  ].join('');
}

String's split() splits a string into an array of strings. It accepts as an argument the character(s) you would like to split on. The argument can be either a string or a regular expression. You can read more about split() here.

Array's join() combines all the members of an array into a string. You can read more about join() here.

With those three functions defined in Modash.js, we're ready to export our module.

At the bottom of Modash.js, we first create the object that encapsulates our methods:


const Modash = {
  truncate,
  capitalize,
  camelCase,
};

And then we export it:


export default Modash;

We'll write our testing code for this section inside of the file Modash.test.js. Open up that file in your text editor now.

ES6: Import/export with Babel

Our package.json already includes Babel. In addition, we're including a Babel plug-in, babel-plugin-transform-es2015-modules-commonjs.

This package will let us use the ES6 import/export syntax. Importantly, we specify it as a Babel plugin inside the project's .babelrc:


// basics/.babelrc
{
  "plugins": ["transform-es2015-modules-commonjs"]
}

With this plugin in place, we can now export a module from one file and import it into another.

However, note that this solution won't work in the browser. It works locally in the Node runtime, which is fine for the purposes of writing tests for our Modash library. But to support this in the browser requires additional tooling. As we mentioned in the last chapter, ES6 module support in the browser is one of our primary motivations for using Webpack.

Writing the first spec

Our test suite will import the library we're writing tests for, Modash. We'll call methods on that library and make assertions on how the methods should behave.

At the top of Modash.test.js, let's first import our library:


import Modash from './Modash';

Our first assertion will be for the method truncate. We're going to assert that when given a string over the supplied length, truncate returns a truncated string.

First, we setup the test:


const string = 'there was one catch, and that was CATCH-22';
const actual = Modash.truncate(string, 19);
const expected = 'there was one catch...';

We're declaring our sample test string, string. We then set two variables: actual and expected. In test suites, actual is what we call the behavior that was observed. In this case, it's what Modash.truncate actually returned. expected is the value we are expecting.

Next, we make our test's assertion. We'll print a message indicating whether truncate passed or failed:


if (actual !== expected) {
  console.log(
    `[FAIL] Expected \`truncate()\` to return '${expected}', got '${actual}'`
  );
} else {
  console.log('[PASS] `truncate()`.');
}

Try it out

We can run our test suite at this stage in the command line. Save the file Modash.test.js and run the following from the testing/basics folder:


./node_modules/.bin/babel-node Modash.test.js

Executing this, we see a [PASS] message printed to the console. If you'd like, you can modify the truncate function in Modash.js to observe this test failing:


Test passing


Example of what it looks like when test fails

The assertEqual() function

Let's write some tests for the other two methods in Modash.

For all our tests, we're going to be following a similar pattern. We're going to have some assertion that checks if actual equals expected. We'll print a message to the console that indicates whether the function under test passed or failed.

To avoid this code duplication, we'll write a helper function, assertEqual(). assertEqual() will check equality between both its arguments. The function will then write a console message, indicating if the spec passed or failed.

At the top of Modash.test.js, below the import statement for Modash, declare assertEqual:


import Modash from './Modash';

// leanpub-start-insert
function assertEqual(description, actual, expected) {
  if (actual === expected) {
    console.log(`[PASS] ${description}`);
  } else {
    console.log(`[FAIL] ${description}`);
    console.log(`\tactual: '${actual}'`);
    console.log(`\texpected: '${expected}'`);
  }
}
// leanpub-end-insert

A tab is represented as the \t character in JavaScript.

With assertEqual defined, let's re-write our first test spec. We're going to re-use the variables actual, expected, and string throughout the test suite, so we'll use the let declaration so that we can redefine them:


let actual;
let expected;
let string;

string = 'there was one catch, and that was CATCH-22';
actual = Modash.truncate(string, 19);
expected = 'there was one catch...';

assertEqual('`truncate()`: truncates a string', actual, expected);

If you were to run Modash.test.js now, you'd note that things are working just as before. The console output is just slightly different:


Test passing

With our assert function written, let's write some more tests.

Let's write one more assertion for truncate. The function should return a string as-is if it's less than the supplied length. We'll use the same string. Write this assertion below the current one:


actual = Modash.truncate(string, string.length);
expected = string;

assertEqual('`truncate()`: no-ops if <= length', actual, expected);

Next, let's write an assertion for capitalize. We can continue to use the same string:


actual = Modash.capitalize(string);
expected = 'There was one catch, and that was catch-22';

assertEqual('`capitalize()`: capitalizes the string', actual, expected);

Given the example string we're using, this assertion tests both aspects of capitalize: That it capitalizes the first letter in the string and that it converts the rest to lowercase.

Last, we'll write our assertions for camelCase. We'll test this function with two different strings. One will be delimited by spaces and the other by underscores.

The assertion for spaces:


string = 'customer responded at';
actual = Modash.camelCase(string);
expected = 'customerRespondedAt';

assertEqual('`camelCase()`: string with spaces', actual, expected);

And for underscores:


string = 'customer_responded_at';
actual = Modash.camelCase(string);
expected = 'customerRespondedAt';

assertEqual('`camelCase()`: string with underscores', actual, expected);

Try it out

Save Modash.test.js. From the console, run the test suite:


./node_modules/.bin/babel-node Modash.test.js

Tests passing

Feel free to tweak either the expected values for each assertion or break the library and watch the tests fail.

Our miniature assertion framework is clear but limited. It's hard to imagine how it would be both maintainable and scalable for a more complex app or module. And while assertEqual() works fine for checking the equality of strings, we'll want to make more complex assertions when working with objects or arrays. For instance, we might want to check if an object contains a particular property or an array a particular element.

What is Jest?

JavaScript has a variety of testing libraries that pack a bunch of great features. These libraries help us organize our test suite in a robust, maintainable manner. Many of these libraries accomplish the same domain of tasks but with different approaches.

An example of testing libraries you may have heard of or worked with are Mocha, Jasmine, QUnit, Chai, and Tape.

We like to think of testing libraries as having three major components:

  • The test runner. This is what you execute in the command-line. The test runner is responsible for finding your tests, running them, and reporting results back to you in the console.
  • A domain-specific language for organizing your tests. As we'll see, these functions help us perform common tasks like orchestrating setup and teardown before and after tests run.
  • An assertion library. The assert functions provided by these libraries help us easily make otherwise complex assertions, like checking equality between JavaScript objects or the presence of certain elements in an array.

React developers have the option to use any JavaScript testing framework they'd like for their tests. In this book, we'll focus on one in particular: Jest.

Facebook created and maintains Jest. If you've used other JavaScript testing frameworks or even testing frameworks in other programming languages, you'll likely find Jest quite familiar.

For assertions, Jest uses Jasmine's assertion library. If you've used Jasmine before, you'll be pleased to know the syntax is exactly the same.

Later in the chapter, we explore what's arguably Jest's biggest difference from other JavaScript testing frameworks: mocking.

Using Jest

Inside of testing/basics/package.json, you'll note that Jest is already included.

As of Jest 15, Jest will consider any file that ends with *.test.js or *.spec.js a test. Because our file is named Modash.test.js, we don't have to do anything special to instruct Jest that this is a test file.

We'll rewrite the specs for Modash using Jest.

Jest 15

If you've used an older version of Jest before, you might be surprised that our tests do not have to be inside a __tests__/ folder. Furthermore, later in the chapter, you'll notice that Jest's auto-mocking appears to be turned off.

Jest 15 shipped new defaults for Jest. These changes were motivated by a desire to make Jest easier for new developers to begin using while maintaining Jest's philosophy to require as little configuration as necessary.

You can read about all the changes in this blog post. Relevant to this chapter:

  • In addition to looking under __tests__/ for test files Jest also looks for files matching *.test.js or *.spec.js
  • Auto-mocking is disabled by default