How to Build a Minimal Mocha Test Runner from Scratch
This article walks through the design and implementation of a lightweight Mocha-like test framework for Node.js, covering automated testing concepts, core functions, asynchronous support, suite‑test tree construction, result collection, and verification with code examples and diagrams.
Big Factory Tech Weekly
This article is contributed by members of the Feishu aPaaS Growth R&D team.
The aPaaS Growth team focuses on user‑perceivable, macro‑level aPaaS application building processes, tenant and application governance, aiming to create a smooth "application delivery" experience, improve ecosystem support, and boost overall performance to drive user growth.
Preface
What Is Automated Testing
Automated testing is often a difficult DevOps step because test code is tightly coupled with business logic; writing test code can be more labor‑intensive than the actual code. It is best suited for medium‑ to long‑term business components or foundational libraries. Types include unit, integration, and end‑to‑end tests, with Mocha being a popular unit‑testing framework.
What Is Mocha
Mocha is a Node.js test framework that supports synchronous and asynchronous tests, as well as TDD and BDD styles. Although its source is large due to many features (HTML reports, plugins, etc.), most projects only use its core "test" capability; other features can be replaced by third‑party libraries.
Preparation
Understanding Mocha
We will implement a simplified Mocha by focusing on its core functions. Below is a sample code that determines a value’s type using Object.prototype.toString:
// mocha-demo/index.js
const toString = Object.prototype.toString;
function getTag(value) {
if (value == null) {
return value === undefined ? '[object Undefined]' : '[object Null]';
}
return toString.call(value);
}
module.exports = { getTag };The corresponding test case (using Node’s native assert and BDD style) is:
// test/getTag.spec.js
const assert = require('assert');
const { getTag } = require('../index');
describe('Check: getTag function', function() {
before(function() { console.log('😁 before hook'); });
describe('Normal flow', function() {
it('returns [object JSON]', function(done) {
setTimeout(() => {
assert.equal(getTag(JSON), '[object JSON]');
done();
}, 1000);
});
it('returns [object Number]', function() {
assert.equal(getTag(1), '[object Number]');
});
});
describe('Error flow', function() {
it('returns [object Undefined]', function() {
assert.equal(getTag(undefined), '[object Undefined]');
});
});
after(function() { console.log('😭 after hook'); });
});Running the test produces the screenshot below:
Note: More Mocha usage details can be found at Mocha – the fun, simple, flexible JavaScript test framework.
Core Functions
Mocha provides two core functions: describe (a test suite) and it (a test case). describe groups related tests, while it executes an individual test.
Testing Styles
The example uses BDD style, which emphasizes behavior first. TDD (test‑driven development) emphasizes writing tests before code. Mocha defaults to BDD, and we will implement BDD in our simplified version.
Hook Functions
before: runs before a suite. after: runs after a suite. beforeEach: runs before each test. afterEach: runs after each test.
Hooks are useful for mocking data, collecting test data, or customizing reports.
Supporting Asynchronous
Mocha supports asynchronous tests via returning a Promise or calling a done callback. Example:
it('returns [object JSON]', function(done) {
setTimeout(() => {
assert.equal(getTag(JSON), '[object JSON]');
done();
}, 1000);
});Execution Result and Order
Tests run in a strict outer‑to‑inner, top‑to‑bottom order, independent of hook declaration order.
Design
Directory Structure Design
├── index.js # code under test
├── mocha # simplified Mocha implementation
│ ├── index.js # entry file
│ ├── interfaces
│ │ ├── bdd.js # BDD style implementation
│ │ └── index.js # export interfaces
│ ├── reporters
│ │ ├── index.js
│ │ └── spec.js
│ └── src
│ ├── mocha.js # Mocha class controlling flow
│ ├── runner.js # Runner class executing tests
│ ├── suite.js # Suite class handling describe
│ ├── test.js # Test class handling it
│ └── utils.js # helper utilities
├── package.json
└── test # test cases
└── getTag.spec.jsOverall Process Design
Collect test cases into a Suite‑Test tree.
Traverse the tree to execute each test.
Collect execution results.
class Mocha {
constructor() {}
run() {}
}
module.exports = Mocha;Implementation
Creating Root Node
Instantiate a root Suite in the Mocha constructor.
// mocha/src/mocha.js
const Suite = require('./suite');
class Mocha {
constructor() {
this.rootSuite = new Suite(null, '');
}
run() {}
}
module.exports = Mocha;Global API Mounting
Expose BDD APIs (describe, it, before, after, etc.) globally.
// mocha/interfaces/bdd.js
module.exports = function(context, root) {
const suites = [root];
context.describe = context.context = function(title, fn) {
const cur = suites[0];
const suite = new Suite(cur, title);
suites.unshift(suite);
fn.call(suite);
suites.shift();
};
// other APIs defined similarly
};Importing Test Files
Recursively read test files from a fixed directory and require them.
// mocha/src/utils.js
const path = require('path');
const fs = require('fs');
module.exports.findCaseFile = function(filepath) { /* implementation */ };
module.exports.adaptPromise = function(fn) { /* implementation */ };Creating Suite‑Test Tree
During describe/it execution, build a tree of Suite and Test objects.
// mocha/interfaces/bdd.js (excerpt)
context.it = context.specify = function(title, fn) {
const cur = suites[0];
const test = new Test(title, adaptPromise(fn));
cur.tests.push(test);
};Supporting Asynchronous
Wrap test and hook callbacks with a Promise adapter to handle both Promise returns and done callbacks.
// mocha/src/utils.js (excerpt)
module.exports.adaptPromise = function(fn) {
return () => new Promise(resolve => {
if (fn.length === 0) {
try {
const ret = fn();
if (ret instanceof Promise) return ret.then(resolve, resolve);
resolve();
} catch (e) { resolve(e); }
} else {
function done(err) { resolve(err); }
fn(done);
}
});
};Executing Test Cases
Runner traverses the Suite‑Test tree, runs hooks and tests using async/await.
// mocha/src/runner.js
class Runner {
async run(root) { /* emit start, runSuite, emit end */ }
async runSuite(suite) { /* beforeAll, tests, child suites, afterAll */ }
async runTest(test) { /* beforeEach chain, test.fn(), afterEach chain */ }
}
module.exports = Runner;Collecting Test Execution Results
The Runner extends EventEmitter and emits events for run start/end, suite start/end, pass, and fail. A simple reporter listens to these events and prints colored summaries.
// mocha/reporter/spec.js
module.exports = function(runner) {
// listen to EVENT_PASS, EVENT_FAIL, etc., and output summary
};Verification
A failing test case is added to demonstrate error reporting.
// failing test example
const assert = require('assert');
const { getTag } = require('../index');
describe('Check: getTag function', function() {
// ... previous tests ...
it('returns [object Object] (expected failure)', function() {
assert.equal(getTag([]), '[object Object]'); // actually returns [object Array]
});
});Afterword
Mocha’s core ideas are simple, but the full framework offers extensive extensibility. Combining Mocha with libraries like Chai, Sinon, and Istanbul enables powerful, customizable testing pipelines.
References
https://github.com/mochajs/mocha
https://mochajs.org/
Reference Materials
[1] Mocha – the fun, simple, flexible JavaScript test framework: https://mochajs.org/
- END -
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
