Reader small image

You're reading from  Building Enterprise JavaScript Applications

Product typeBook
Published inSep 2018
Reading LevelIntermediate
PublisherPackt
ISBN-139781788477321
Edition1st Edition
Languages
Right arrow
Author (1)
Daniel Li
Daniel Li
author image
Daniel Li

Daniel Li is a full-stack JavaScript developer at Nexmo. Previously, he was also the Managing Director of Brew, a digital agency in Hong Kong that specializes in MeteorJS. A proponent of knowledge-sharing and open source, Daniel has written over 100 blog posts and in-depth tutorials, helping hundreds of thousands of readers navigate the world of JavaScript and the web.
Read more about Daniel Li

Right arrow

Chapter 8. Writing Unit/Integration Tests

We have now done as much as we can to modularize our code base, but how much confidence do we have in each of the modules? If one of the E2E tests fails, how would we pinpoint the source of the error? How do we know which module is faulty?

We need a lower level of testing that works at the module level to ensure they work as distinct, standalone units—we need unit tests. Likewise, we should test that multiple units can work well together as a larger logical unit; to do that, we need to also implement some integration tests.

By following this chapter, you will be able to do the following:

  • Write unit and integration tests using Mocha
  • Record function calls with spies, and simulate behavior with stubs, both provided by the Sinon library
  • Stub out dependencies in unit tests using dependency injection (DI) or monkey patching
  • Measuring test coverage with Istanbul/nyc

Picking a testing framework


While there's only one de facto testing framework for E2E tests for JavaScript (Cucumber), there are several popular testing frameworks for unit and integration tests, namely Jasmine (jasmine.github.io), Mocha (mochajs.org), Jest (jestjs.io), and AVA (github.com/avajs/ava).

We will be using Mocha for this book, but let's understand the rationale behind that decision. As always, there are pros and cons for each choice:

  • Maturity: Jasmine and Mocha have been around for the longest, and for many years were the only two viable testing frameworks for JavaScript and Node. Jest and AVA are the new kids on the block. Generally, the maturity of a library correlates with the number of features and the level of support.
  • Popularity: Generally, the more popular a library is, the larger the community, and the higher likelihood of receiving support when things go awry. In terms of popularity, let's examine several metrics (correct as of September 7, 2018):
    • GitHub stars@ Jest (20...

Structuring our test files


Next, we are going to write our unit tests, but where should we put them? There are generally two approaches:

  • Placing all tests for the application in a top-level test/ directory
  • Placing the unit tests for a module of code next to the module itself, and using a generic test directory only for application-level integration tests (for example, testing integration with external resources such as databases)

The second approach (as shown in the following example) is better as it keeps each module truly separated in the filesystem:

$ tree
.
├── src
│   └── feature
│       ├── index.js
│       └── index.unit.test.js
└── test
    ├── db.integration.test.js
    └── app.integration.test.js

Furthermore, we're going to use the .test.js extension to indicate that a file contains tests (although using .spec.js is also a common convention). We will be even more explicit and specify the type of test in the extension itself; that is, using unit.test.js for unit test, andintegration...

Writing our first unit test


Let's write unit tests for the generateValidationErrorMessage function. But first, let's convert our src/validators/errors/messages.js file into its own directory so that we can group the implementation and test code together in the same directory:

$ cd src/validators/errors
$ mkdir messages
$ mv messages.js messages/index.js
$ touch messages/index.unit.test.js

Next, in index.unit.test.js, import the assert library and our index.js file:

import assert from 'assert';
import generateValidationErrorMessage from '.';

Now, we are ready to write our tests.

Describing the expected behavior

When we installed the mocha npm package, it provided us with the mocha command to execute our tests. When we run mocha, it will inject several functions, including describe and it, as global variables into the test environment. The describe function allows us to group relevant test cases together, and the it function defines the actual test case.

Inside index.unit.tests.js, let's define our...

Completing our first unit test suite


We have only covered a single scenario with our first unit test. Therefore, we should write more tests to cover every scenario. Try completing the unit test suite for generateValidationErrorMessage yourself; once you are ready, compare your solution with the following one:

import assert from 'assert';
import generateValidationErrorMessage from '.';

describe('generateValidationErrorMessage', function () {
  it('should return the correct string when error.keyword is "required"', function () {
    const errors = [{
      keyword: 'required',
      dataPath: '.test.path',
      params: {
        missingProperty: 'property',
      },
    }];
    const actualErrorMessage = generateValidationErrorMessage(errors);
    const expectedErrorMessage = "The '.test.path.property' field is missing";
    assert.equal(actualErrorMessage, expectedErrorMessage);
  });
  it('should return the correct string when error.keyword is "type"', function () {
    const errors = ...

Unit testing ValidationError


Next, let's focus on testing the ValidationError class. Once again, we will move the validation.js file into its own director:

$ cd src/validators/errors/ && \
  mkdir validation-error && \
  mv validation-error.js validation-error/index.js && \
  cd ../../../

Now, create a new file atsrc/validators/errors/validation-error/index.unit.test.js to house our unit tests:

import assert from 'assert';
import ValidationError from '.';

describe('ValidationError', function () {
  it('should be a subclass of Error', function () {
    const validationError = new ValidationError();
    assert.equal(validationError instanceof Error, true);
  });
  describe('constructor', function () {
    it('should make the constructor parameter accessible via the `message` property of the instance', function () {
      const TEST_ERROR = 'TEST_ERROR';
      const validationError = new ValidationError(TEST_ERROR);
      assert.equal(validationError.message, TEST_ERROR...

Unit testing middleware


Next, we are going to test our middleware functions, starting with the checkEmptyPayload middleware. Like we did previously, move the middleware module into its own directory:

$ cd src/middlewares/ && \
  mkdir check-empty-payload && \
  mv check-empty-payload.js check-empty-payload/index.js && \
  touch check-empty-payload/index.unit.test.js && \
  cd ../../

Then, inside src/middlewares/check-content-type.js/index.unit.test.js, lay out the skeleton of our first test:

import assert from 'assert';
import checkEmptyPayload from '.';

describe('checkEmptyPayload', function () {
  describe('When req.method is not one of POST, PATCH or PUT', function () {
    it('should not modify res', function () {
      // Assert that `res` has not been modified
    });

    it('should call next() once', function () {
      // Assert that `next` has been called once
    });
  });});

The purpose of the checkEmptyPayload middleware is to ensure that the POST...

Unit testing the request handler


First, we'll move the src/handlers/users/create.js module into its own directory. Then, we will correct the file paths specified in the import statements to point to the correct file. Lastly, we will create an index.unit.test.js file next to our module to house the unit tests.

Let's take a look at the createUser function inside our request handler module. It has the following structure:

import create from '../../../engines/users/create';
function createUser(req, res, db) {
  create(req, db)
 .then(onFulfilled, onRejected)
 .catch(...)
}

First, it will call the create function that was imported from src/engines/users/create/index.js. Based on the result, it will invoke either the onFulfilled or onRejected callbacks inside the thenblock.

Although our createUser function depends on the create function, when writing a unit test, our test should test only the relevant unit, not its dependencies. Therefore, if the result of our tests relies on thecreate function, we...

Unit testing our engine


Next, let's test our create engine function. Like our previous createUser request handler, the src/engines/users/create/index.js module contains two import statements, which makes it difficult to test. Therefore, just like before, we must pull these dependencies out, and import them back into src/index.js:

import createUserValidator from './validators/users/create';
...
const handlerToValidatorMap = new Map([
  [createUserHandler, createUserValidator],
]);
...
app.post('/users', injectHandlerDependencies(createUserHandler, client, handlerToEngineMap, handlerToValidatorMap, ValidationError));

Then, update the injectHandlerDependencies function to inject the validator function into the handler:

function injectHandlerDependencies(
  handler, db, handlerToEngineMap, handlerToValidatorMap, ValidationError,
) {
  const engine = handlerToEngineMap.get(handler);
const validator = handlerToValidatorMap.get(handler);
  return (req, res) => { handler(req, res, db, engine,...

Integration testing our engine


So far, we have been retrofitting our code with unit tests, which test each unit individually, independent of external dependencies. However, it's also important to have confidence that different units are compatible with each other. This is where integration tests are useful. So, let's add some integration tests to our User Create engine that'll test its interaction with the database.

First, let's update our npm scripts to include a test:integration script. We'll also update the glob file in our test:unit npm to be more specific and select only unit tests. Lastly, update the test script to run the integration tests after the unit tests:

"test": "yarn run test:unit && yarn run test:integration && yarn run test:e2e",
"test:unit": "mocha 'src/**/*.unit.test.js' --require @babel/register",
"test:integration": "dotenv -e envs/test.env -e envs/.env mocha -- src/**/*.integration.test.js' --require @babel/register",

The dotenv mocha part will run Mocha...

Adding test coverage


At the beginning of our TDD process, we wrote E2E tests first and used them to drive development. However, for unit and integration tests, we actually retrofitted them back into our implementation. Therefore, it's very likely that we missed some scenarios that we should have tested for.

To remedy this practical problem, we can summon the help of test coverage tools. A test coverage tool will run your tests and record all the lines of code that were executed; it will then compare this with the total number of lines in your source file to return a percentage coverage. For example, if my module contains 100 lines of code, and my tests only ran 85 lines of my module code, then my test coverage is 85%. This may mean that I have dead code or that I missed certain use cases. Once I know that some of my tests are not covering all of my code, I can then go back and add more test cases.

The de facto test coverage framework for JavaScript is istanbul (github.com/gotwarlost/istanbul...

Finishing up


We have now modularized and tested the code for the Create User feature. Therefore, now is a good time to merge our current create-user/refactor-modules branch into the create-user/main branch. Since this also completes the Create User feature, we should merge the create-user/main feature branch back into the dev branch:

$ git checkout create-user/main
$ git merge --no-ff create-user/refactor-modules
$ git checkout dev
$ git merge --no-ff create-user/main

Summary


Over the course of the preceding three chapters, we have shown you how to write E2E tests, use them to drive the development of your feature, modularize your code wherever possible, and then increase confidence in your code by covering modules with unit and integration tests.

In the next chapter, you will be tasked with implementing the rest of the features by yourself. We will outline some principles of API design that you should follow, and you can always reference our sample code bundle, but the next chapter is where you truly get to practice this process independently.

"Learning is an active process. We learn by doing. Only knowledge that is used sticks in your mind."

- Dale Carnegie, author of the book How to Win Friends and Influence People

lock icon
The rest of the chapter is locked
You have been reading a chapter from
Building Enterprise JavaScript Applications
Published in: Sep 2018Publisher: PacktISBN-13: 9781788477321
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $15.99/month. Cancel anytime

Author (1)

author image
Daniel Li

Daniel Li is a full-stack JavaScript developer at Nexmo. Previously, he was also the Managing Director of Brew, a digital agency in Hong Kong that specializes in MeteorJS. A proponent of knowledge-sharing and open source, Daniel has written over 100 blog posts and in-depth tutorials, helping hundreds of thousands of readers navigate the world of JavaScript and the web.
Read more about Daniel Li