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.

WecTeam
WecTeam
WecTeam
Build a Simple Mocha Clone from Scratch: Step‑by‑Step Guide

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.json

Key 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/)
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaScriptTestingNode.jsBDDFrameworkmochaRunner
WecTeam
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.