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.

ELab Team
ELab Team
ELab Team
How to Build a Minimal Mocha Test Runner from Scratch

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:

Mocha test result
Mocha test result
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.

Test execution order
Test execution 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.js

Overall 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);
};
Suite-Test tree
Suite-Test tree

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

Result collection diagram
Result collection diagram

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]
  });
});
Failing test output
Failing test output

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 -

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.

JavaScriptAutomationTestingNode.jsmochaTest Runner
ELab Team
Written by

ELab Team

Sharing fresh technical insights

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.