Testability is a major concern in software development. A piece of code whose functionality cannot be properly verified is hardly worth it’s storage space. Therefore, no matter what technology you’re using you will sooner or later (ideally sooner) face the question “How do I test my stuff”? I started developing with NodeJs and Express a while ago. Sure enough, pretty soon I had to figure out how to address this question.

In the following Node.js tutorial I will use a simple example app to present some tools and practices I found extremely helpful when I started off testing my code. This Node.js tutorial is split into 2 parts. Part 1 gives an overview of the test tools I am using and explains how to isolate your code for under testing. Part 2 goes into how to effectively test your routing with parameterized tests, how to populate your database with test data, and how to set up end to end testing of your application. The code for this article is available in NodeTestProject on Gitlab

The example app

The example application is structured in a classic three layer architecture. Besides advantages such as separation of concerns and exchangeability, this structure also has benefits for testing parts in separation.
project_flow

project_structure2– The presentation layer module is responsible for the routing of incoming requests. It exposes a REST API with a single endpoint that allows for creation and retrieval of users.
– The business layer module contains the business logic for storing and retrieving a user.
– The data access layer module is an abstraction layer that hides where the data actually is stored. The example app uses Mongoose and MongoDB, but this could also be a relational database, a webservice, or a file.

The architecture is also reflected in the project structure. Each layer has its own top level folder. All test files and test data reside in the test-folder. The lib-folder contains all general purpose functionality.

 

Tools

From the plethora of test tools out there, I chose the following ones:

  • Mocha is a test framework for defining test suites. It has lots of useful features for synchronous and asynchronous testing. Mocha comes with a built in test runner and also works with most third party test runners.
  • Chai is a flexible assertion library that works with most test frameworks. It provides interfaces for both expectation-style and assertion-style testing.
  • Sinon is a mocking framework. It helps with replacing module dependencies with different kinds of fake objects. The most common ones are spies, which simply monitor the usage of a dependency, and stubs, which replace dependencies with self defined functionality.
  • Mockery is a dependency mocking framework that allows you to register fake modules to be loaded instead of real ones. This is helpful if the code under test imports modules that perform initialization tasks when they are loaded, e.g. database modules.
  • Supertest is a framework for executing HTTP requests. It is based on Superagent and abstracts most of Superagent’s complexity, allowing test requests to be quickly defined.

Other tools that come in handy:

  • lodash is a JavaScript utility library that every developer should have in her or his toolbox. It offers plenty of handy helper functions for dealing with all types of common tasks.
  • it-each extends mocha to allow the creation of parameterized tests.  This is particularly useful when the only thing that changes between test cases is the input data. it-each takes one test case and automatically runs it for every entry in a parameter array.
  • rootpath registers the application’s root directory as a global root directory. This allows developers to load modules by stating a path relative to the root directory from everywhere in the project tree. For instance, instead of using paths that depend on where the source file resides, like ‘../logic/userModule’ or ‘../../logic/userModule’, modules can be loaded using ‘logic/userModule’ where “logic” is a top level folder of the application.
  • mocha-mongoose is a test helper that automatically removes all data from the database. This is helpful when you are testing database queries and need to start off with an empty database.

All tools are available via npm.

Debugging

Tests might fail unexpectedly, especially in the beginning of development. In these cases, it is important to have a good debugger available. Node-inspector is the debugger of choice for Node applications. It can be used to debug your Node server as well as your mocha tests. The only prerequisite is that the Chrome browser is installed.
Node_inspector
For debugging simply open a console in your application root directory(important if you’re using rootpath) and enter

node-debug _mocha --no-timeouts PATH/TO/YOUR/MOCHA/TEST

Your test will be loaded in Chrome and the test runner will be stopped automatically at the first line of code. You then can freely set break points throughout your code and step through to see what’s wrong.

Node-inspector can be installed via npm.

 

Unit testing

Unit testing is an essential step in developing quality software. A unit test runs a small piece of code in isolation from its dependencies. The code piece is treated as a black box: its public functions get called with a set of test parameters and the return values are compared to expected results. This makes the discovery of problems with the code fairly easy.

As an example, we want to test the business layer module “userModule” and its public functions, validateUsername(), createUser(), and getUser(). The first step is to separate the module from its external dependencies to avoid any without any side effects. This can be done using Sinon and Mockery. “userModule” only dependency is the data access layer module “dbConnector”, which we will replace with a dummy.

unit_business

A common way in Node to replace dependencies is to load them and then substitute the functions that are called from our test code with stubs. But sometimes it is not desirable to load a real dependency-module because its initialization might have side effects. “dbConnector” is a good example. On initialization, it establishes a connection to the database. This would be troublesome for testing purposes since the database would have to be available every time the test is executed.

This problem can be solved with Mockery. Mockery allows us to register dummy modules for given load paths. It extends Node’s _load function which is internally called when modules are loaded. Mockery first checks if a dummy module was registered for the load path it received. If so, it returns the dummy. Otherwise the it forwards the request to the regular loading functionality.

First we create a dummy object that contains two Sinon stubs which replace the “dbConnector” functions. The stubs simply call the passed in callback functions with the predefined arguments: username and userId.

    //set up DB stub functions
    getUser = sinon.stub();
    getUser.callsArgWith(1, null, {
      "userId": userId,
      "username": username
    });

    createUser = sinon.stub();
    createUser.callsArgWith(2, null, {
      "userId": userId
    });

    db = {};
    db.getUser = getUser;
    db.createUser = createUser;

Next we enable Mockery and register the dummy.

  //enable mockery and register DB stub object
    mockery.enable({
      warnOnReplace: true,
      warnOnUnregistered: false,
      useCleanCache: false
    });

    mockery.registerMock('data/dbConnector', db);

    //load userModule AFTER registering the DB stub so it does not load the real one
    userModule = require("logic/userModule");

One downside of Mockery is that it does not use Node’s path discovery. When a dummy is registered, Mockery treats the load path as simple string key. To retrieve the dummy module, the exact same string must be passed. For example, if a dummy was registered for the path “test/myDummy” it will not be found for “../test/myDummy”. To mitigate this problem we use “rootpath”. It allows us to declare all module paths relative to the root directory of the application. Keep in mind that rootpath treats the folder the application was started in as the root directory. Therefore, the mocha tests must be executed from the root directory of the application. Otherwise your modules might not be found.

Now that we registered the “dbConnector” dummy we can load “userModule” and create our tests. This involves setting up test parameters, calling the function under test, and asserting the results. “userModule”s functions are regular middleware functions that take a request object, a response object, and a callback function as attributes. All test cases use the request object to pass test parameters. For the response object and the callback function there are three scenarios:

  • The middleware function sends a response using one of the response objects functions (e.g. res.json()) and we are not expecting our test to cause an exception. In this case the response object is used to assert the test criteria using chai expression language. As callback function we can pass mocha’s “done”-function. “done” behaves similarly to Node’s next-function: when it’s called with an argument, it signals mocha that an asynchronous test failed.
      it("should get a user", function(done) {
        req.params = {
          "userId": userId
        };
    
        //setup response assuming that you return the response via res.json
        res.json = function(respObj) {
          expect(respObj.user).to.not.be.undefined;
          expect(respObj.user.username).to.equal(username);
          //Additional tests go here...
    
          done();
        };
    
        userModule.getUser(req, res, done);
      });
  • The middleware function sends a response back and we are expecting an exception. In this case the response object is used to indicate that the test failed since there should have been an exception before a response function was reached. The callback-function’s job is to assert the test criteria using chai expression language.
      it("should fail getting a user when no userId was passed", function(done) {
        req.params = {};
        
        res.json = function(respObj) {
          if (respObj) done(respObj);
          else done(new Error());
        };
        //setup response assuming that you return the response via res.json
        userModule.getUser(req, res, function(respObj) {
          expect(respObj).to.not.be.undefined;
          expect(respObj.message).to.equal("missing userId");
          //Additional tests go here...
    
          done();
        });
      });
  • The middleware function does not send a response back. In this case the callback-function needs to assert the test criteria, whether we are expecting an exception or not.
    it("should throw an exception if username is too long", function(done) {
        req.body = {
          "username": "This username is too long!!!"
        };
    
        userModule.validateUsername(req, res, function(err) {
          if (err) {
            expect(err.message).to.equal("username too long");
            done();
          } else {
            done(new Error());
          }
        });
      });

The test cases for the userModule don’t add much value as they test very basic functionality. However, they are sufficient for illustrating how to stub your dependencies and where to put your assertions.

That’s it for part 1 of this Node.js tutorial. In part 2 of this tutorial, we will look at routing tests, database tests, and end to end tests. Stay tuned

The code for this article can be downloaded in nodeTestProject on Gitlab

Part 2 of this article is available Node.js Tutorial (Part 2) – Step by Step Testing Node Apps.