March 12, 2021
Code Mode


3 Tips for Testing with Mocha / Sinon

Hello, my name is Meng! I am currently a tech lead / software engineer at avail car sharing.

As most engineers know, writing supplementary tests is considered a staple when you contribute to a codebase.  At avail, we commonly use the mocha and sinon javascript libraries to write unit / integration tests in our back-end node codebase.  In the past couple of years, through personal trials and tribulations, I have curated 3 tips that have proved really useful for my teams and me when using the mocha and sinon libraries.

Firstly, what is Mocha and Sinon?  Mocha is a javascript testing framework. Sinon is a standalone javascript library that provides out-of-the-box spies, stubs and mocks for testing.

This article assumes a beginner knowledge of the javascript libraries [mocha] and [sinon].

Tip No. 1: Understand the Execution Order of Mocha Hooks.

When running mocha tests, hooks are incredibly useful.  They can help you set up and tear down fixtures before and after each test or a suite of tests.  In mocha, there are 4 main hooks you can use:

  • before
  • beforeEach
  • after
  • afterEach

If you don't know what mocha hooks are, do get acquainted with them here.  Now, let's focus on the execution order of hooks. According to mocha's description, the execution order of the [mocha hooks] may seem pretty straightforward, but what happens if you have many nested describe / context blocks (describe / context blocks are the same thing in mocha - they are used as a way to group / organize tests - for more info on describe / context click here). Or what happens if you have several hooks scattered around all execution contexts?  What is the traversal order of the hooks then?

To start, here is the general traversal order of your mocha file with hooks:

  1. statements not within a hook or a test (order: top-down; global to local)
  2. before hooks (order: top-down; global to local)
  3. beforeEach hooks (order: top-down; global to local)
  4. tests (tests invoked by it and specify) (order: top-down; global to local)
  5. afterEach hooks (order: bottom-up; local to global)
  6. after hooks (order: bottom-up; local to global)

Let's do an example!  Can you guess the execution order below?

describe('1.0', function () {

 before(function () {});
 beforeEach(function () {});
 after(function () {});
 afterEach(function () {});

 describe('1.1', function () {

   it('', function () {});
   it('', function () {});
 });
});
See below for the answer*:
describe('1.0', function () {
 //1
 before(function () {}); //3
 beforeEach(function () {}); //4 //7
 after(function () {}); //10
 afterEach(function () {}); //6 //9

 describe('1.1', function () {
   //2
   it('', function () {}); //5
   it('', function () {}); //8
 });
});
  • click this repl link here to try the above with logs (be sure to fork the repo, and in the repl shell run npm run test-exec-order-demo)

How about a more complicated example with nested describe contexts?  Can you guess the execution order below?

describe('1.0', function () {

 before(function () {});
 beforeEach(function () {});
 after(function () {});
 afterEach(function () {});

 describe('1.1', function () {

   before(function () {});
   beforeEach(function () {});
   after(function () {});
   afterEach(function () {});
     it('', function () {});
     it('', function () {});

     describe('1.1.1', function () {

       it('', function () {});
       it('', function () {});
     });
 });
});

See below for the answer**:

describe('1.0', function () {
 //1
 before(function () {}); //4
 beforeEach(function () {}); //6 //11 //16 //21
 after(function () {}); //27
 afterEach(function () {}); //10 //15 //20 //25

 describe('1.1', function () {
   //2
   before(function () {}); //5
   beforeEach(function () {}); //7 //12 //17 //22
   after(function () {}); //26
   afterEach(function () {}); //9 //14 //19 //24
     it('', function () {}); //8
     it('', function () {}); //13

     describe('1.1.1', function () {
       //3
       it('', function () {}); //18
       it('', function () {}); //23
     });
 });
});
  • click this repl link here to try the above with logs (be sure to fork the repo, and in the repl shell run npm run test-exec-order-adv-demo)

Tip No. 2: Isolate / Troubleshoot Tests by Using mocha's .only.  

Sometimes, you may find yourself working with a rather large mocha file - with not only several tests embedded, but several nested tests as well.  Most of the time, when coding up a bug fix or a new feature - to start with, you only care about a specific set of tests within the larger subset of tests and not ALL the tests at once (running all the tests each time when you are only working on a specific slice of code can be excessively time consuming!).  **.only** to the rescue!  In mocha, to isolate a specific test while you run your code, simply append .only to any it or describe mocha block.

of note: when you are done testing in isolation, DO NOT forget to REMOVE the .only method so the rest of your tests can run!  (pro tip: install the eslint-plugin-mocha plugin and turn on this linting rule so you are always reminded of any stray .only invocations)

Here is an example:

describe('1.0', function () {

 before(function () {});
 beforeEach(function () {});
 after(function () {});
 afterEach(function () {});

 describe('1.1', function () {

   before(function () {});
   beforeEach(function () {});
   after(function () {});
   afterEach(function () {});
     it('1.1a', function () {console.log('1.1a test being run')});
     it('1.1b', function () {console.log('1.1b test being run')});

     describe.only('1.1.1', function () {

       it('1.1.1a test', function () {console.log('1.1.1a test being run')});
       it('1.1.1b test', function () {console.log('1.1.1b test being run')});
     });
 });
});
  • click this repl link here to try the above (be sure to fork the repo, and in the repl shell run npm run test-.only-demo)

Note in the above example, only test 1.1.1a and test 1.1.1b tests end up being run (tests 1.1a and 1.1b are not run due to .only in the 1.1.1 describe block) .

Tip No. 3: Stub Default Exported Functions Using Sinon.

Stubbing is a very useful tool when you are trying to isolate functionality while you conjure up your unit tests.  Almost all of our unit test files at avail leverage the use of stubs (if you don't know what a stub is, please click here for more info).  Sometimes, we find ourselves needing to stub default exported functions (functions that are exported like module.exports = foo and NOT like module.exports = { foo }).

But how would you stub a default exported function like module.exports = foo when sinon's stub api is as follows: const stub = sandbox.stub(object, "method") ?

There are many ways to working around this including restructuring the functions in your files or using proxyquire. However, in cases where we cannot restructure the functions from 3rd party libraries and / or your team want to minimize the amount of 3rd party libraries used, let's try to figure out a 3rd solution.

First, let's take a look at the problem and what I mean by a default exported function.  Suppose you had a function (main) in an index.js file that returns the sum of an array:

// index.js

const sum = require('./util/sum');

const main = (array) => sum(array);

module.exports = {
 main
}

Let's also suppose the main function (as depicted above) uses the following imported sum function:

// util/sum.js

const sum = (array) => array.reduce((currVal, accumul) => currVal + accumul);

module.exports = sum;

Note the sum.js file above has a default export of the sum function.  Now, what if we wanted to stub the sum.js function by having it always return the value of 50 whenever the sum function is invoked during our mocha tests?

As a reminder, to utilize sinon's stub function - the stub api is as follows: const stub = sandbox.stub(object, "method"). As we only have the object (sum), but not the method,  how can we try to stub the sum function in sum.js?

// defaultExportDemo.spec.js

const { main } = require('../index');
const assert = require('assert');
const sinon = require('sinon');
const sandbox = sinon.createSandbox();
const sum = require('../util/sum')

describe('default export sinon demo', function () {
 it('default export sinon demo', function () {

 const sumStub = sandbox.stub(sum, '?').returns(50); // what should the (?) method be here?
 const result = main([1,2,3]);
 assert.deepEqual(result, 50);
 });
});

Here is what it boils down to: how can we get sinon to automatically return a stubbed value of 50 each time the sum function is invoked?  In other words, is there a way, where when node requires sum.js in index.js - we can then force a return value of 50? To force an assignment of a module, we can use the [require.cache method] in node.

Let's take a look at what implementing the require.cache method would look like:

const { main } = require('../index');
const assert = require('assert');
const sinon = require('sinon');
const sandbox = sinon.createSandbox();
const sum = require('../util/sum');

describe('default export sinon demo', function () {
 it('default export sinon demo', function () {

   require.cache[require.resolve('../util/sum')] = {
     exports: sandbox.stub().returns(50),
   };

   const result = main([1,2,3]);
   assert.deepEqual(result, 50);  // fail, result = 6 not 50
 });
});

Hmm - note, the above assertion fails, and the actual value of result is 6 (which indicates an unsuccessful stub).

What could be going on here?  Let's put away this thought for a moment and talk about how the require function works in node.  Each time a file is loaded, node recursively loads up any modules that are invoked with require.  Take our example below:

//defaultExportDemo.spec.js

const { main } = require('../index');
const assert = require('assert');
const sinon = require('sinon');
const sandbox = sinon.createSandbox();
const sum = require('../util/sum');

....

When running the above test file, node will first load up the index.js module and ALSO load up any modules invoked WITHIN the index.js module.  Let's take another look at the index.js file:

//../index.js

const sum = require('./util/sum');

const main = (array) => sum(array);

module.exports = {
 main
}

Let's recap: when we run the defaultExportDemo.spec.js test file, node not only loads up index.js, but node also loads up any modules index.js loads (including the sum function in sum.js).

Now, knowing this, let's go back to our last thought - why is the below assertion failing?

const { main } = require('../index');
const assert = require('assert');
const sinon = require('sinon');
const sandbox = sinon.createSandbox();
const sum = require('../util/sum');

describe('default export sinon demo', function () {
 it('default export sinon demo', function () {

   require.cache[require.resolve('../util/sum')] = {
     exports: sandbox.stub().returns(50),
   };

   const result = main([1,2,3]);
   assert.deepEqual(result, 50);  // fail, result = 6 not 50
 });
});

You guessed it. index.js is being loaded too early - which, in turn means the main function is loaded too early causing the main function to be unaware of the new value of the sum module cache.  What can we do here then?  How about we just require index.js right after we re-assign the sum file?

let { main } = require('../index');
const assert = require('assert');
const sinon = require('sinon');
const sandbox = sinon.createSandbox();
const sum = require('../util/sum');

describe('default export sinon demo', function () {
 it('default export sinon demo', function () {

   require.cache[require.resolve('../util/sum')] = {
     exports: sandbox.stub().returns(50),
   };

   ({ main } = require('../index'));

   const result = main([1,2,3]);
   assert.deepEqual(result, 50);  // fail, result = 6 not 50
 });
});

Failed again!  Why? Because  node will never load modules more than once due to multiple require invocations (meaning, once a module is loaded, the module is cached and never re-loaded again - no matter how many times you require it again).  Yikes.  Okay, how can we force a reload of a previously required module? To force the reload of a previously required module, we need to delete the module's cache and then re-load the module again (we can do this by using the delete require.cache method and then require the module again - although, please beware the delete require.cache method will delete the module's cache globally - so any reference to the deleted module in any test suite / context thereafter will fail if you forget to, subsequently, require the module).  See below for the final implementation:

let { main } = require('../index');
const assert = require('assert');
const sinon = require('sinon');
const sandbox = sinon.createSandbox();
const sum = require('../util/sum');

describe('default export sinon demo', function () {
 it('default export sinon demo', function () {

   delete require.cache[require.resolve('../index')];
   require.cache[require.resolve('../util/sum')] = {
     exports: sandbox.stub().returns(50),
   };
   ({main} = require('../index'));

   const result = main([1,2,3]);
   assert.deepEqual(result, 50);
 });
});
  • click this repl link here to try the above (be sure to fork the repo, and in the repl shell run npm run test-default-export-demo)

Like this article? Interested in working with us?  Join avail!

Browse Jobs
Get Started

Related Posts