Testing JavaScript NodeJS files

This article will demonstrate how to test your back-end JavaScript files in NodeJS using the libraries Sinon, Mocha and Chai. Sinon is often installed together with Mocha and Chai, but it is not required.

Our testing libraries of choice

  • Sinon is a library that offers you functionality to create test doubles and spies.
  • Mocha is a test framework that allows you to describe('') your test suites and run your it('should do something') tests within test suites.
  • Chai is an assertion library that for example allows you to expect(something).to.be.true in a very expressive way.

But let’s start with some testing concepts.

Types of tests

  • Unit Tests test very specific, low-level pieces of functionality for example if a function returns the expected value.
  • Integration Tests ensure that the individual pieces of your application work together correctly. For example if a method invocation successfully calls an API which in turn stores a value in the database.
    • Single-service integration Tests test a single piece or ‘service’ of your application end to end, independent of the other pieces. For example to test our server API code we would mock the database and use an integration testing framework to make calls to the API instead of the front-end. The bad thing is that this approach is susceptible to Outside Change, in other words: If the database API changes then the Server API tests would still pass, because we mocked them out.
    • Boundary Integration Tests test the communication between different pieces of your application. For example testing that the front-end is calling the Server API correctly, or that the Server API is calling the database API correctly. Don’t use test doubles for this. Disadvantage is that tests run slower.
  • End-to-End (E2E) Tests ensure that your entire application works as seen from the viewpoint of a user. The user does not know about how the internals work, that’s why this is sometimes called Black Box Testing.

When it comes to speaking how many tests there should be for each test type then you can assume that you will write mostly Unit Tests, a moderate amount of Integration Tests and a relatively small amount of E2E Tests.

Test cycles

Each test starts off by writing a failing test, then you write production code to make the test pass and finally you refactor the code. This is called red-green-refactor.

When you bring Integration and Unit Tests together you proceed like this: Start by writing a failing Integration Test, then move into the inner cycle of Unit Tests and do red-green-refactor until the outer cycle (the Integration Test) passes. At this time you stop development until you write another failing integration test.

Setting up a testing environment

You need a testing environment (a test runner), a testing framework and an assertion library.

Testing terminology

Test spy

A Test Spy is a function that spies (or reports) on a function, but it does not change your function in any way. A spy is pretty much like placing console.log() in your function body. A spy is used to answer questions about a function because it records

  • if a function was notCalled
  • if a function got calledWith arguments at least one time
    • or calledWithExactly() arguments
    • or alwaysCalledWith() specific arguments
    • or that the first argument of the first call of a function was ‘foo’
      expect(spy.getCall(0).args[0]).to.equal('foo');
  • how many times a function got called (calledCount).
  • what values a function returned()
  • the value of the this-keyword
  • if a function threw() exceptions
const testSpy = sinon.spy(Crypto, 'randomBytes');

A Spy is great to report back information on a function, but it does not control the behavior of the function.

Test stub

A test stub is like a stunt double in an action movie. It looks the same but it keeps the original actor safe. In the testing world that means that we can replace any calls to external systems, such as databases or file systems, with a stub. The external system will not be invoked when we run the test.

We can even go a step further and have stubs return specific values, throw an exception, resolve or reject a promise or call a separate fake function.

const testStub = sinon.stub(Crypto, 'randomBytes');

A Stub allows you to control/force how a function behaves.

Fakes

A Fake is a combination of a Spy and a Stub, but they are not a universal replacement because they come with differences. Fakes are immutable. Fakes do not automatically replace your real function with your fake function, instead you have to explicitly call a replace function (more about it later).

Mocks

With Mocks you write your expectations first, and then execute the function under test and finally verify if those expectations have been met. Mocks contain all the functionality of a Spy and a Stub and own functions on top.

Doubles

A Double is just a general term for Mock, Fake and Stub.

Some testing rules to follow

  • Never spy on the function under test, instead spy on functions inside the tested function.
  • Tests should not pass or fail based on an external system, instead use stubs to replace the external systems.
  • Tests should be RITE.
    • Readable, even more easy to read than your productive code
    • Isolated, without affecting results of other tests
    • Thorough, to test as many variations as possible. If you can break your production code without breaking your tests, then the tests are not thorough enough.
    • Explicit. meaning all information to run the test is readily available, there is no hidden information or shared state
  • Don’t use Test Doubles as a band-aid for bad code. Refactoring your functions to become pure functions is a good approach to prevent using Test Doubles.
  • Don’t mock what you don’t own, because third-party software changes constantly. Mocking a library, then updating the library version would lead to the situation that your tests pass (because of an outdated mock) while the production code breaks.
  • Never use test doubles in unit tests, because that strongly indicates that your production code is too tightly coupled and needs refactoring

proxyquire

How do we get our “fake” spies / stubs into the “real” code that we want to test? We must rule out modifying the “real” code. The solution is to use proxyquire module. It basically works like this:

const proxyquire = require("proxyquire");

describe('your test suite', function () {
    it('should test something', function () {
        const whateverServiceStub = {
            isValid: () => true
        }

        const anotherStub = {
            sign: () => 'valid_jwt'
        }

        const toTestProxy = proxyquire("../../path/to/file/that-we-want/to-test",
            {
                // key must be exactly what the import/require statement looks like in your real file
                // the value is a stub
                '../whatever/myService.js': whateverServiceStub,
                'jsonwebtoken': anotherStub
            },
        )


        toTestProxy.someMethod();
    });
});

Here is a complete example using Sinon spies:

const Crypto = require("Crypto");
const proxyquire = require("proxyquire");

it('should call Crypto to create a random token', async function() {
      const testSpy = spy(Crypto, 'randomBytes');
      const tokenHandlerProxy = proxyquire('../../src/handlers/tokenHandler', {Crypto});
      tokenHandlerProxy.requestToken();
      expect(testSpy.called).to.be.true;
});

You must restore your spies

A caveat: You cannot spy twice at the same method from within two different tests. So this won’t work:

const Crypto = require("Crypto");

it('should call Crypto to create a random token', function() {
      const testSpy = spy(Crypto, 'randomBytes');
      // ...
});

it('should do something else with Crypto', function() {
      const testSpy = spy(Crypto, 'randomBytes');
      // ...
});

Doing this will throw a Sinon TypeError: Attempted to wrap <your-function> which is already wrapped. To fix that, you have to restore the spy after each test, which can be simply done with:

const sinon = require("sinon");

describe("the test suite", () => {
  afterEach(() => {
    sinon.restore();
  });
})

Letting a stub throw exceptions

How can we test exceptions? In the following example we test code that in reality would throw an exception whenever we tried to write to an already existing file test.txt. We will stub the writeFileSync method of fs, because our goal is to let the stub throw an exception without actually changing the file system. We use proxyquire again as described above.

it('should throw an exception if file already exists', () => {
  const writeStub = sinon.stub(fs, "writeFileSync");
  writeStub.throws(new Error());
  const fileManagement = proxyquire("./file.management", { fs });
  expect(() => fileManagement.createFile("test.txt")).to.throw();
})

Note, that this will always throw an error whenever we invoke writeFileSync. Maybe not what we want.

Throw an error on specific invocations

We want to specify exactly on which call writeFileSync will throw an error. There are two way to do this, via onCall() or by using withArgs().

The following example uses onCall(0) with throws() which will throw an exception only on its first invocation. We also have to define that on any subsequent invocation the stub returns(undefined):

writeStub = sinon.stub(fs, "writeFileSync");
const fileManagement = proxyquire("./fileManangement", { fs });

writeStub.onCall(0).throws(new Error());
writeStub.returns(undefined);

We can be more specific and test if a stub was called withArgs():

writeStub = sinon.stub(fs, "writeFileSync");
const fileManagement = proxyquire("./fileManangement", { fs });

writeStub.withArgs("./data/test.txt").throws(new Error());
writeStub.returns(undefined);

Stubbing asynchronous functions

Asynchronous functions can handle results in different way, such as via a callback or a Promise. Here we have a typical asynchronous function using a callback:

const fs = require('fs');

getAllFiles: cb => {
  fs.readdir("./data", cb);
}

The callback function uses Node’s typical error-first-style:

(err, data) => { }

To test this we use:

myStub.yields(null, myValue);

Again: Asynchronous callback-style tests do not use returns(), instead they use yields(). Here is a complete example in which we stub readdir:

describe('', () => {
  it('should return a list of files', () => {
    const readStub = sinon.stub(fs, 'readdir');
    const fileManager = proxyquire('./file-management', { fs });
    readStub.yields(null, ["test.txt"]);

    fileManagement.getAllFiles((err, data) => {
      // use eql, not equal (because we don't want strict comparison)
      expect(data).to.eql(["test.txt"]);
    })
  });
})

Promisified async functions

Use promisify to convert a callback-style result into a Promise-style result.

As already mentioned, there are different styles on how a async result is returned: callback or Promise. Newer versions of NodeJS offer a Promise-style return value for most methods. But let’s say there is a method that does only return its values callback-style. Then you would want to convert a callback-style method into a Promise-style method by using promisify:

const util = require('util');
const fs = require('fs');

const stat = util.promisify(fs.stat);

async function callStat() {
  const stats = await stat('.');
  console.log(`This directory is owned by ${stats.uid}`);
}

When you use promisify in your real code, then you also have to adjust your test code:

describe('', () => {
  it('should return a list of files', () => {
    const readStub = sinon.stub(fs, 'readdir');

    const util = {
      promisify: sinon.stub().returns(readStub)
    }

    const fileManager = proxyquire('./file-management', { fs, util });
    readStub.yields(null, ["test.txt"]);

    readStub.resolves(["test.txt"]);

    return fileManagement.getAllFilesPromise()
      .then(files => expect(files).to.eql(["test.txt"]));
  });
})

In this example we called the promise-style variant getAllFilesPromise(). We created a util that we passed into proxyquire and told readStub to resolve with ["text.txt"].

Calling fake functions

You can call fake functions on a Stub:

readStub.callsFake((...args) => { })

Fakes

Use replace()

You use Fakes similar to a Spy or Stub, but you have to replace() the real function with the fake function manually.

StubFake
sinon.stub(fs, "writeFileSync");sinon.replace(fs, "writeFileSync", writeFake);
it("should create a new file", () => {
  const writeFake = sinon.fake(fs.writeFileSync);
  sinon.replace(fs, "writeFileSync", writeFake);
  const fileManagement = proxyquire("./file/management", { fs });
  
  fileManagement.createFile("test.txt");
  expect(writeFake.calledWith("./data/test.txt")).to.be.true;
});

With this, the Fake acts like a Spy (not a Stub) which gets clear once you run this test function twice: First it succeeds, but on the second run it fails. A clear sign, that it actually runs the real code to create a file. But what we want is to also make the Fake behave like a Stub. The solution is to add behavior to a Fake.

Immutability of a Fake

Another important thing: Because of the immutability of a Fake, this does not work:

const readFake = sinon.fake;
// does not work: you try to modify an immutable property
sinon.yields(null, ["test.txt"]);

Instead, writing it in one line does the trick:

const readFake = sinon.fake.yields(null, ["test.txt"]);

Adding behavior

Adding behavior to a Fake makes the Fake act as a Stub (additionally to act as a Spy). To add behavior we use sinon.fake and not sinon.fake(). A subtle but important difference.

it("should create a new file", () => {
  const writeFake = sinon.fake.throws(new Error());
  sinon.replace(fs, "writeFileSync", writeFake);
  const fileManagement = proxyquire("./file/management", { fs });
  
  fileManagement.createFile("test.txt");
  expect(writeFake.calledWith("./data/test.txt")).to.be.true;
});

Fakes with async functions

describe('', () => {
  it('should return a list of files', () => {
    const readFake = sinon.fake.yields(null, ["test.txt"]);
    sinon.replace(fs, "readdir", readFake);

    const fileManager = proxyquire('./file-management', { fs });

    return fileManagement.getAllFiles(
      expect(data).to.eql(["test.txt"]);
  });
})

Limitations

onCall() and withArgs() does not work with Fakes. A Fake must always have the same behavior. callsFake(), addBehavior() and callsThrough() do not work.

Mocks

Mocks mock the entire module, not single functions.

describe("File Management Mocks", () => {
  afterEach(() => {
    sinon.release();
  });

  it("should call writeFileSync when creating a file", () => {
    const writeMock = sinon.mock(fs);
    writeMock.expects("writeFileSync").once():

    const fileManagement = proxyquire("./file/management", { fs });
    fileManagement.createFile("test.txt");

    writeMock.verify();
  });
})

Just like a Stub, a Mock does not create the actual file.

In the following example we assume there is a real function called createFileSafe that throws an error if a file with the passed in file name already exists an creates a new file with an appended number.

describe("File Management Mocks", () => {
  afterEach(() => {
    sinon.release();
  });

  it("should call writeFileSync when creating a file", () => {
    const writeMock = sinon.mock(fs);

    writeMock.expects("writeFileSync").withArgs("test.txt").throws();
    writeMock.expects("writeFileSync").withArgs("test.txt1").once();
    writeMock.expects("readdirSync").returns([test.txt]).once();

    const fileManagement = proxyquire("./file/management", { fs });
    fileManagement.createFileSafe("test.txt");

    writeMock.verify();
  });
})

Verifying a function was not called

describe("File Management Mocks", () => {
  afterEach(() => {
    sinon.release();
  });

  it("should never call writeFileSync when the file is empty", () => {
    const writeMock = sinon.mock(fs);

    writeMock.expects("writeFileSync").never();

    const fileManagement = proxyquire("./file/management", { fs });
    try {
      fileManagement.createFile();
    } catch(err) {}

    writeMock.verify();
  });
})

Other Mock functions include twice(), thrice(), exactly(number), atLeast(number), atMost(number). withExactArgs().

About Author

Mathias Bothe Contact me

I am Mathias, born 38 years ago in Heidelberg, Germany. Today I am living in Munich and Stockholm. I am a passionate IT freelancer with more than 14 years experience in programming, especially in developing web based applications for companies that range from small startups to the big players out there. I am founder of bosy.com, creator of the security service platform BosyProtect© and initiator of several other software projects.