Testing JavaScript NodeJS files with Mocha and Chai
  • Mocha allows you to describe('') your test suites and run your it('should do something') tests within test suites and runs in NodeJS and the browser
  • Chai allows you to expect(something).to.be.true in a very expressive way
yarn add --dev mocha chai sinon

Testing setup

By default, Mocha automatically looks for files with .js.mjs or .cjs extension inside a directory test (but not in subfolders), relative to the current working directory.

Mocha can run tests in serial or parallel mode.

A test suite is created using describe(), they can be nested. A test is created using it() within a test suite.

Browser testing setup

This article is about testing NodeJS files, but if you want to run browser tests you can quickly initialize a browser-based setup: Running the following will create a index.html, mocha.css, mocha.js and tests.spec.js:

mocha init .

Testing sync vs async code

Synchronous code

When testing synchronous code, Mocha will automatically continue on to the next test. In this example, we’re using Node.js’ built-in assert module.

var assert = require('assert');
describe('Array', function () {
  describe('#indexOf()', function () {
    it('should return -1 when the value is not present', function () {
      assert.equal([1, 2, 3].indexOf(4), -1);
    });
  });
});
"scripts": {
  "test": "mocha"
}
npm test

Callback-style async code

Call the done() function to indicate when the async call is done.

describe('User', function () {
  describe('#save()', function () {
    it('should save without error', function (done) {
      var user = new User('Luna');
      user.save(function (err) {
        if (err) done(err);
        else done();
      });
    });
  });
});

Promise-style async code

The following examples rely on chai-as-promised. To set it up:

yarn add --dev chai-as-promised
const chai = require("chai");
const expect = chai.expect;
const chaiAsPromised = require("chai-as-promised");
chai.use(chaiAsPromised);

Expecting a Promise to resolve / return a value

beforeEach(function () {
  return db.clear().then(function () {
    return db.save([tobi, loki, jane]);
  });
});

describe('#find()', function () {
  it('respond with matching records', function () {
    return db.find({type: 'User'}).should.eventually.have.length(3);

    // or without a return use
    // doSomethingAsync().should.eventually.equal("foo").notify(done);
  });
});

Expecting a Promise to reject / throw an exception

it("should throw error if no value is given", function () {
    return expect(thisWillFail()).to.be.rejectedWith("Need string as input value");
    // or
    // expect(thisWillFail()).to.be.rejectedWith("Need string as input value").notify(done);
});

async/await-style async code

beforeEach(async function () {
  await db.clear();
  await db.save([tobi, loki, jane]);
});

describe('#find()', function () {
  it('responds with matching records', async function () {
    const users = await db.find({type: 'User'});
    users.should.have.length(3);
  });
});

Managing test execution

Pending tests

// a pending test
it('should return -1 when the value is not present');

Skipping tests

it.skip('should return -1 unless present', function () {
  // this test will not be run
});

Running only specific tests

// an exclusive test
describe.only('#indexOf()', function () {
    // ...
});

// or

it.only('should return -1 unless present', function () {
   // ...
});

Retrying tests

describe('retries', function () {
  // Retry all tests in this suite up to 4 times
  this.retries(4);

  beforeEach(function () {
    browser.get('http://www.yahoo.com');
  });

  it('should succeed on the 3rd try', function () {
    // Specify this test to only retry up to 2 times
    this.retries(2);
    expect($('.foo').isDisplayed()).to.eventually.be.true;
  });
});

Test timeouts

You can apply timeouts on suite-level, test-level or hook-level, or disable via this.timeout(0). This will be inherited by all nested suites and test-cases that do not override the value.

describe('a suite of tests', function () {
  this.timeout(500);

  it('should take less than 500ms', function (done) {
    setTimeout(done, 300);
  });

  it('should take less than 500ms as well', function (done) {
    setTimeout(done, 250);
  });
});

Dynamically generating Tests

Testing the same thing but just with different parameters can be easily done by generating tests dynamically with pure JavaScript. The following example even works with “right-click run” features of IDEs, which usually do not work if we used a .forEach handler instead:

describe('add()', function () {
  const testAdd = ({args, expected}) =>
    function () {
      const res = add(args);
      assert.strictEqual(res, expected);
    };

  it('correctly adds 2 args', testAdd({args: [1, 2], expected: 3}));
  it('correctly adds 3 args', testAdd({args: [1, 2, 3], expected: 6}));
  it('correctly adds 4 args', testAdd({args: [1, 2, 3, 4], expected: 10}));
});

Lifecycle hooks

A hook simply runs code either once before/after the first or last test or every time before/after each test. Hooks can be sync or async.

before, after, beforeEach, afterEach

describe('hooks', function () {
  before(function () {
    // runs once before the first test in this block
  });

  after(function () {
    // runs once after the last test in this block
  });

  beforeEach(function () {
    // runs before each test in this block
  });

  afterEach(function () {
    // runs after each test in this block
  });

  // test cases
});

Example of an async hook

You can use one of the async styles (callback, promise, async/await) also for hooks. The following example clears a database and saves three predefined users for each test (fixture):

describe('Connection', function () {
  var db = new Connection(),
    tobi = new User('tobi'),
    loki = new User('loki'),
    jane = new User('jane');

  beforeEach(function (done) {
    db.clear(function (err) {
      if (err) return done(err);
      db.save([tobi, loki, jane], done);
    });
  });

  describe('#find()', function () {
    it('respond with matching records', function (done) {
      db.find({type: 'User'}, function (err, res) {
        if (err) return done(err);
        res.should.have.length(3);
        done();
      });
    });
  });
});

Hook descriptions

Instead of an anonymous hook function, you can use one of the following to describe your hooks to pinpoint errors in your tests:

beforeEach(function namedFun() {
  // beforeEach:namedFun
});

beforeEach('some description', function () {
  // beforeEach:some description
});

Root hooks

In some cases, you may want a hook before (or after) every test in every file. These are called root hooks. A Root Hook Plugin file is a script which exports (via module.exports) a mochaHooks property. The following hook resets the SinonJS sandbox:

const sinon = require("sinon");
exports.mochaHooks = {
    afterEach() {
        // Restores the default sandbox after every test
        sinon.restore();
    },
};

Finally, use --require test/hooks.js or even better, use a config file when running your tests:

module.exports = {
    "require": "test/hooks.js"
}

Global fixtures

Global fixtures are good for spinning up a server, opening a socket, or otherwise creating a resource that your tests will repeatedly access via I/O. Just as with global hooks, global fixtures must either use --require to be loaded or you define them in a config file.

let server;

export const mochaGlobalSetup = async () => {
  server = await startSomeServer({port: process.env.TEST_PORT});
  console.log(`server running on port ${server.port}`);
};

export const mochaGlobalTeardown = async () => {
  await server.stop();
  console.log('server stopped!');
};

Then connect to the database in your tests:

import {connect} from 'my-server-connector-thingy';

describe('my API', function () {
  let connection;

  before(async function () {
    connection = await connect({port: process.env.TEST_PORT});
  });

  it('should be a nice API', function () {
    // assertions here
  });

  after(async function () {
    return connection.close();
  });
});

Asserting tests with Chai

The reason to use Chai is to replace the Node.js standard assert function. Chai offers your three styles of writing assertions, it’s best to stick to one: assert, expect or should. For the full list of options check the BDD API on chai’s official website.

// two ways to add a custom error message
expect(1).to.be.a('string', 'nooo why fail??');
expect(1, 'nooo why fail??').to.be.a('string');
// Asserts that the target is strictly (===) equal to the given val
expect(1).to.equal(1);
expect('foo').to.equal('foo');

expect('foo').to.be.a('string');
expect({a: 1}).to.be.an('object');

expect(null).to.be.a('null');
expect(null).to.be.null;

expect(undefined).to.be.an('undefined');
expect(undefined).to.be.undefined;

expect(NaN).to.be.NaN;

expect(new Error).to.be.an('error');
expect(Promise.resolve()).to.be.a('promise');
expect(new Float32Array).to.be.a('float32array');
expect(Symbol()).to.be.a('symbol');

expect(true).to.be.true;
expect(false).to.be.false;

expect([1, 2, 3]).to.be.an('array').that.includes(2);
expect([]).to.be.an('array').that.is.empty;

expect(function () {}).to.not.throw();
expect({a: 1}).to.not.have.property('b');
expect([1, 2]).to.be.an('array').that.does.not.include(3);

// Target object deeply (but not strictly) equals `{a: 1}`
expect({a: 1}).to.deep.equal({a: 1});
expect({a: 1}).to.not.equal({a: 1});

// Target array deeply (but not strictly) includes `{a: 1}`
expect([{a: 1}]).to.deep.include({a: 1});
expect([{a: 1}]).to.not.include({a: 1});

// Target object deeply (but not strictly) includes `x: {a: 1}`
expect({x: {a: 1}}).to.deep.include({x: {a: 1}});
expect({x: {a: 1}}).to.not.include({x: {a: 1}});

// Target array deeply (but not strictly) has member `{a: 1}`
expect([{a: 1}]).to.have.deep.members([{a: 1}]);
expect([{a: 1}]).to.not.have.members([{a: 1}]);

// Target set deeply (but not strictly) has key `{a: 1}`
expect(new Set([{a: 1}])).to.have.deep.keys([{a: 1}]);
expect(new Set([{a: 1}])).to.not.have.keys([{a: 1}]);

// Target object deeply (but not strictly) has property `x: {a: 1}`
expect({x: {a: 1}}).to.have.deep.property('x', {a: 1});
expect({x: {a: 1}}).to.not.have.property('x', {a: 1});

expect({a: 1, b: 2}).to.not.have.any.keys('c', 'd');

expect({a: 1, b: 2}).to.have.all.keys('a', 'b');

About Author

Mathias Bothe To my job profile

I am Mathias, born 39 years ago in Heidelberg, Germany. Today I am living in Munich and Stockholm. I am a passionate IT freelancer with more than 15 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.