Mocha unit testing pattern: test-suite setup-code for file-separated test
By: Rich Gwozdz
If you are using Mocha for Node.js testing than you may be familiar with the before() and beforeAll() functions. The former is designed to run once per describe block (regardless of how many tests the block contains), while the latter runs once before every test within the describe block. See more here.
It follows that if you want a before() function that executes once before the entire test suite, the entire test suite needs to be encapsulated in a single describe with a top-level before:
/* abcSpec.js */
describe('Mocha Test Suite', function () {
const chai = require('chai');
const expect = require('chai').expect;
var randomNumber;
before(function() {
// Do this one time before any test is executed
randomNumber = math.rand();
});
describe('A test for math.floor', function(){
it('Should equal 0', function (done) {
expect(math.floor(randomNumber) === 0).to.equal(true);
});
});
describe('A test for math.ceil', function(){
it('Should equal 1', function (done) {
expect(math.ceil(randomNumber) === 1).to.equal(true);
});
});
});
As your test suite grows, it may become desirable to split it into separate test specification files. If we had the two trivial tests above split up across separate files, we would have to repeat the before() code in each of those files. This would not be a huge effort or inefficiency for the random number example above, but you could probably imagine cases where you wouldn’t want to either duplicate or execute the same before() across dozens of files.
One real-world example we ran into was testing a data-api that was authenticated with a token issued by a third-party identity store. The token also had user-specific information that was necessary for testing some of the endpoints. I was unable to reproduce the structure of the third party’s token in the form of a mock. Thus the API code to decode it failed. The alternative was to request a real token in the before( ) of each test file. But this didn’t scale — once we create a crtical number of test-files, the third-party identity service began rejecting us due rate limits — we were only allow x-number of token requests per minute. We needed one token, from one request, that we could distribute to all Mocha tests.
We developed a new Mocha test pattern to facilitate execution of a block of setup-code exactly once before the test suite begins. The gist is to wrap each Mocha test specification file in a function assigned to module.exports and then require each of these files in a master “test-runner” file that Mocha executes rather than the test files themselves. In the test-runner, you execute the require functions AFTER you have executed the setup-code. If the setup-code returns a value needed in the test, you pass it during the require.
The following is example of the function-wrapped Mocha test-specifications that you can pass a setup value to, in this a case a random number generated by our setup-code:
/* abcSpec.js */
'use strict';
const chai = require('chai');
const expect = require('chai').expect;
module.exports = function (randomNumber) {
describe('A test for math.floor', function(){
it('Should equal 0', function (done) {
expect(math.floor(randomNumber) === 0).to.equal(true);
});
});
};
The following snippet is an example of a test-runner.js that executes the setup-code and requires, executes the file above:
/* test-runner.js */
'use strict';
const fs = require('fs');
// Get all test specification files from directory
var testFiles = fs.readdirSync(__dirname + '/test-specs');
// Setup-code - Do this one time before any test suite started
var randomNumber = math.rand();
// Require the abcTest.js test and pass it the randomNumber
require('./test-specs/abcSpec.js')(randomNumber);
// Mocha command to run tests (since Mocha doesn't access them directly)
run();
Mocha by default tries to execute all javascript files in the root test directory. However, you can point Mocha specifically to test-runner.js:
mocha ./test/test-runner.js
As you will probably have more than one test-specification file, you probably want to store them all in a sub-directory of test (e.g. test-specs) and have test-runner.js require all the files in that sub-directory:
/* test-runner.js */
'use strict';
const fs = require('fs');
// Get all test specification files from directory
var testFiles = fs.readdirSync(__dirname + '/test-specs');
// Setup-code - Do this one time before any test suite started
var randomNumber = math.rand();
// Require all the tests and supply with the same random number
testFiles.forEach(function (file) {
require('./test-specs/' + file)(randomNumber);
});
// Mocha command to run tests
run();
In summary your test directory should look like this:
├── test
├── test-runner.js
└── test-specs
└── abcSpec.js
That’s all there is to it. By pointing Mocha to your test-runner.js you can generate data or execute setup code before your test-suite starts but also pass the generated data to the test-specifications files.