Build a Simple Mocha Clone from Scratch: Step‑by‑Step Guide
This article walks through creating a lightweight Mocha‑like testing framework in JavaScript, covering BDD‑style APIs, directory layout, suite and test class design, hook handling, asynchronous support, a runner that traverses the suite‑test tree, and a reporter that outputs test results.
Preface
Mocha is the most popular JavaScript testing framework. Understanding its internals helps deepen knowledge of automated testing, but reading the source can be daunting due to advanced patterns and edge‑case handling. Instead of dissecting Mocha directly, we will build a simple Mocha clone from scratch.
What We Will Implement
BDD‑style testing with describe / it to define test suites and cases.
Hook mechanism: before, after, beforeEach, afterEach.
Simple test report output.
Mocha’s BDD Testing
Mocha supports BDD/TDD; the default is BDD, which focuses on requirements first. A basic BDD test looks like:
describe('Array', function() {
describe('#indexOf()', function() {
before(function() { /* ... */ });
it('should return -1 when not present', function() { /* ... */ });
it('should return the index when present', function() { /* ... */ });
after(function() { /* ... */ });
});
});The core BDD APIs are: describe/context: defines a suite. it/specify: defines a test case. before, after, beforeEach, afterEach: hook functions.
Start
We create a project simple-mocha with this structure:
├─ mocha/
│ ├─ index.js
│ ├─ src/
│ ├─ interfaces/
│ └─ reporters/
├─ test/
└─ package.jsonKey files: mocha/: source code of the clone. mocha/index.js: entry point. mocha/src/: core classes. mocha/interfaces/: BDD API implementation. mocha/reporters/: output reporters. test/: test specifications.
The package.json simply defines the script npm test to run node mocha/index.js.
Module Design
The Mocha class creates a root suite, mounts BDD APIs to the global object, loads test files, and runs the suite tree.
class Mocha {
constructor() {}
run() {}
}
module.exports = Mocha;Two core classes represent the tree:
class Suite {
constructor(props) {
this.title = props.title;
this.suites = [];
this.tests = [];
this.parent = props.parent;
this._beforeAll = [];
this._afterAll = [];
this._beforeEach = [];
this._afterEach = [];
if (props.parent instanceof Suite) {
props.parent.suites.push(this);
}
}
}
module.exports = Suite; class Test {
constructor(props) {
this.title = props.title;
this.fn = props.fn;
}
}
module.exports = Test;Collecting Test Cases
A root suite is created, BDD APIs are attached globally, and test files under test/ are required. The utils.lookupFiles helper resolves files or directories recursively.
module.exports.lookupFiles = function lookupFiles(filepath) {
// file or directory handling logic
return files;
};Asynchronous Execution
Test functions may return a Promise or accept a done callback. utils.adaptPromise wraps any function into a Promise‑based one.
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);
} else {
resolve();
}
} catch (error) { resolve(error); }
} else {
function done(error) { resolve(error); }
fn(done);
}
});
};Test Runner
The Runner extends EventEmitter and emits events for the run lifecycle. It traverses the suite‑test tree, executing hooks, tests, and child suites in the correct order.
class Runner extends EventEmitter {
constructor() { super(); this.suites = []; }
async run(root) { this.emit(constants.EVENT_RUN_BEGIN); await this.runSuite(root); this.emit(constants.EVENT_RUN_END); }
async runSuite(suite) { /* beforeAll, tests, child suites, afterAll */ }
async runTest(test) { /* beforeEach chain, test.fn, afterEach chain */ }
}
module.exports = Runner;Reporter
The reporter subscribes to runner events and prints a hierarchical, colored report.
module.exports = function (runner) {
let indents = 0, passes = 0, failures = 0;
function indent(i = 0) { return Array(indents + i).join(' '); }
runner.on(constants.EVENT_RUN_BEGIN, () => console.log());
runner.on(constants.EVENT_SUITE_BEGIN, suite => { ++indents; console.log(indent(), suite.title); });
runner.on(constants.EVENT_SUITE_END, () => { --indents; if (indents == 1) console.log(); });
runner.on(constants.EVENT_PASS, title => { passes++; console.log(indent(1) + '✓ ' + title); });
runner.on(constants.EVENT_FAIL, title => { failures++; console.log(indent(1) + '× ' + title); });
runner.once(constants.EVENT_RUN_END, () => {
console.log(`${passes} passing`);
console.log(`${failures} failing`);
});
};Verification
A sample test suite demonstrates the framework. Running npm test produces output showing passed and failed cases, confirming the simple Mocha clone works.
// test/test.spec.js
const assert = require('assert');
describe('Array', function () {
describe('#indexOf()', function () {
it('should return -1 when not present', function () {
assert.equal(-1, [1, 2, 3].indexOf(4));
});
it('should return the index when present', function () {
assert.equal(-1, [1, 2, 3].indexOf(3));
});
});
describe('#every()', function () {
it('should return true when all items are satisfied', function () {
assert.equal(true, [1, 2, 3].every(item => !isNaN(item)));
});
});
});The execution result shows three passing tests and one failing test, illustrating hook handling, async support, and reporting.
Conclusion
By following this guide you now understand the core mechanisms of Mocha—suite collection, hook orchestration, async adaptation, runner traversal, and reporting. The simplified implementation retains key naming and structure, providing a solid foundation for deeper exploration of Mocha’s full source code.
Mocha official documentation (https://mochajs.org/) BDD and Mocha framework (http://www.moye.me/2014/11/22/bdd_mocha/)
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.
WecTeam
WecTeam (维C团) is the front‑end technology team of JD.com’s Jingxi business unit, focusing on front‑end engineering, web performance optimization, mini‑program and app development, serverless, multi‑platform reuse, and visual building.
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.
