換種方式讀源碼:如何實現一個簡易版的Mocha

做者:黃浩羣node

前言

Mocha 是目前最流行的 JavaScript 測試框架,理解 Mocha 的內部實現原理有助於咱們更深刻地瞭解和學習自動化測試。然而閱讀源碼一直是個讓人望而生畏的過程,大量的高級寫法常常是晦澀難懂,大量的邊緣狀況的處理也十分影響對核心代碼的理解,以致於寫一篇源碼解析事後每每是連本身都看不懂。因此,此次咱們不生啃 Mocha 源碼,換個方式,從零開始一步步實現一個簡易版的 Mocha。npm

咱們將實現什麼?

  • 實現 Mocha 框架的 BDD 風格測試,能經過 describe/it 函數定義一組或單個的測試用例;
  • 實現 Mocha 框架的 Hook 機制,包括 before、after、beforeEach、afterEach;
  • 實現簡單格式的測試報告輸出。

Mocha 的 BDD 測試

Mocha 支持 BDD/TDD 等多種測試風格,默認使用 BDD 接口。BDD(行爲驅動開發)是一種以需求爲導向的敏捷開發方法,相比主張」測試先行「的 TDD(測試驅動開發)而言,它強調」需求先行「,從一個更加宏觀的角度去關注包括開發、QA、需求方在內的多方利益相關者的協做關係,力求讓開發者「作正確的事「。在 Mocha 中,一個簡單的 BDD 式測試用例以下:json

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() {
      // ...
    });
  });
});
複製代碼

Mocha 的 BDD 測試主要包括如下幾個 API:數組

  • describe/context:行爲描述,表明一個測試塊,是一組測試單元的集合;
  • it/specify:描述了一個測試單元,是最小的測試單位;
  • before:Hook 函數,在執行該測試塊以前執行;
  • after:Hook 函數,在執行該測試塊以後執行;
  • beforeEach:Hook 函數,在執行該測試塊中每一個測試單元以前執行;
  • afterEach:Hook 函數,在執行該測試塊中每一個測試單元以後執行。

開始

話很少說,咱們直接開始。promise

1、目錄設計

新建一個項目,命名爲 simple-mocha。目錄結構以下:瀏覽器

├─ mocha/
│   ├─ index.js
│   ├─ src/
│   ├─ interfaces/
│   └─ reporters/
├─ test/
└─ package.json
複製代碼

先對這個目錄結構做簡單解釋:bash

  • mocha/:存放咱們即將實現的 simple-mocha 的源代碼
  • mocha/index.js:simple-mocha 入口
  • mocha/src/:simple-mocha 核心代碼
  • mocha/interfaces/:存放各種風格的測試接口,如 BDD
  • mocha/reporters/:存放用於輸出測試報告的各類 reporter,如 SPEC
  • test/:存放咱們編寫的測試用例
  • package.json

其中 package.json 內容以下:微信

{
  "name": "simple-mocha",
  "version": "1.0.0",
  "description": "a simple mocha for understanding the mechanism of mocha",
  "main": "",
  "scripts": {
    "test": "node mocha/index.js"
  },
  "author": "hankle",
  "license": "ISC"
}
複製代碼

執行 npm test 就能夠啓動執行測試用例。框架

2、模塊設計

Mocha 的 BDD 測試應該是一個」先定義後執行「的過程,這樣才能保證其 Hook 機制正確執行,而與代碼編寫順序無關,所以咱們把整個測試流程分爲兩個階段:收集測試用例(定義)和執行測試用例(執行)。咱們構造了一個 Mocha 類來完成這兩個過程,同時這個類也負責統籌協調其餘各模塊的執行,所以它是整個測試流程的核心。異步

// mocha/src/mocha.js
class Mocha {
  constructor() {}
  run() {}
}

module.exports = Mocha;
複製代碼
// mocha/index.js
const Mocha = require('./src/mocha');

const mocha = new Mocha();
mocha.run();
複製代碼

另外一方面咱們知道,describe 函數描述了一個測試集合,這個測試集合除包括若干測試單元外,還擁有着一些自身的 Hook 函數,維護了一套嚴格的執行流。it 函數描述了一個測試單元,它須要執行測試用例,而且接收斷言結果。這是兩個邏輯複雜的單元,同時須要維護必定的內部狀態,咱們用兩個類(Suite/Test)來分別構造它們。此外咱們能夠看出,BDD 風格的測試用例是一個典型的樹形結構,describe 定義的測試塊能夠包含測試塊,也能夠包含 it 定義的測試單元。因此 Suite/Test 實例還將做爲節點,構造出一棵 suite-test 樹。好比下邊這個測試用例:

describe('Array', function () {
  describe('#indexOf()', function () {
    it('should return -1 when not present', function () {
      // ...
    })
    it('should return the index when present', function () {
      // ...
    })
  })

  describe('#every()', function () {
    it('should return true when all items are satisfied', function () {
      // ...
    })
  })
})
複製代碼

由它構造出來的 suite-test 樹是這樣的:

┌────────────────────────────────────────────────────────┐
                                           ┌─┤        test:"should return -1 when not present"        │
                    ┌────────────────────┐ │ └────────────────────────────────────────────────────────┘
                  ┌─┤ suite:"#indexOf()" ├─┤
                  │ └────────────────────┘ │ ┌────────────────────────────────────────────────────────┐
┌───────────────┐ │                        └─┤       test:"should return the index when present"      │
│ suite:"Array" ├─┤                          └────────────────────────────────────────────────────────┘
└───────────────┘ │
                  │ ┌────────────────────┐   ┌────────────────────────────────────────────────────────┐
                  └─┤  suite:"#every()"  ├───┤ test:"should return true when all items are satisfied" │ 
                    └────────────────────┘   └────────────────────────────────────────────────────────┘
複製代碼

所以,Suite/Test 除了要可以表示 describe/it 以外,還應該可以詮釋這種樹狀結構的父子級關係:

// mocha/src/suite.js
class Suite {
  constructor(props) {
    this.title = props.title;    // Suite名稱,即describe傳入的第一個參數
    this.suites = [];            // 子級suite
    this.tests = [];             // 包含的test
    this.parent = props.parent;  // 父suite
    this._beforeAll = [];        // before hook
    this._afterAll = [];         // after hook
    this._beforeEach = [];       // beforeEach hook
    this._afterEach = [];        // afterEach hook

    if (props.parent instanceof Suite) {
      props.parent.suites.push(this);
    }
  }
}

module.exports = Suite;
複製代碼
// mocha/src/test.js
class Test {
  constructor(props) {
    this.title = props.title;  // Test名稱,即it傳入的第一個參數
    this.fn = props.fn;        // Test的執行函數,即it傳入的第二個參數
  }
}

module.exports = Test;
複製代碼

咱們完善一下目錄結構:

├─ mocha/
│   ├─ index.js
│   ├─ src/
│   │   ├─ mocha.js
│   │   ├─ runner.js
│   │   ├─ suite.js
│   │   ├─ test.js
│   │   └─ utils.js
│   ├─ interfaces/
│   │   ├─ bdd.js
│   │   └─ index.js
│   └─ reporters/
│       ├─ spec.js
│       └─ index.js
├─ test/
└─ package.json
複製代碼

考慮到執行測試用例的過程較爲複雜,咱們把這塊邏輯單獨抽離到 runner.js,它將在執行階段負責調度 suite 和 test 節點並運行測試用例,後續會詳細說到。

3、收集測試用例

收集測試用例環節首先須要建立一個 suite 根節點,並把 API 掛載到全局,而後再執行測試用例文件 *.spec.js 進行用例收集,最終將生成一棵與之結構對應的 suite-test 樹。

一、suite 根節點

咱們先建立一個 suite 實例,做爲整棵 suite-test 樹的根節點,同時它也是咱們收集和執行測試用例的起點。

// mocha/src/mocha.js
const Suite = require('./suite');

class Mocha {
  constructor() {
    // 建立一個suite根節點
    this.rootSuite = new Suite({
      title: '',
      parent: null
    });
  }
  // ...
}
複製代碼
二、BDD API 的全局掛載

在咱們使用 Mocha 編寫測試用例時,咱們不須要手動引入 Mocha 提供的任何模塊,就可以直接使用 describe、it 等一系列 API。那怎麼樣才能實現這一點呢?很簡單,把 API 掛載到 global 對象上就行。所以,咱們須要在執行測試用例文件以前,先將 BDD 風格的 API 所有做全局掛載。

// mocha/src/mocha.js
// ...
const interfaces = require('../interfaces');

class Mocha {
  constructor() {
    // 建立一個根suite
    // ...
    // 使用bdd測試風格,將API掛載到global對象上
    const ui = 'bdd';
    interfaces[ui](global, this.rootSuite);
  }
  // ...
}
複製代碼
// mocha/interfaces/index.js
module.exports.bdd = require('./bdd');
複製代碼
// mocha/interfaces/bdd.js
module.exports = function (context, root) {
  context.describe = context.context = function (title, fn) {}
  context.it = context.specify = function (title, fn) {}
  context.before = function (fn) {}
  context.after = function (fn) {}
  context.beforeEach = function (fn) {}
  context.afterEach = function (fn) {}
}
複製代碼
三、BDD API 的具體實現

咱們先看看 describe 函數怎麼實現。

describe 傳入的 fn 參數是一個函數,它描述了一個測試塊,測試塊包含了若干子測試塊和測試單元。所以咱們須要執行 describe 傳入的 fn 函數,纔可以獲知到它的子層結構,從而構造出一棵完整的 suite-test 樹。而逐層執行 describe 的 fn 函數,本質上就是一個深度優先遍歷的過程,所以咱們須要利用一個棧(stack)來記錄 suite 根節點到當前節點的路徑。

// mocha/interfaces/bdd.js
const Suite = require('../src/suite');
const Test = require('../src/test');

module.exports = function (context, root) {
  // 記錄 suite 根節點到當前節點的路徑
  const suites = [root];

  context.describe = context.context = function (title, fn) {
    const parent = suites[0];
    const suite = new Suite({
      title,
      parent
    });

    suites.unshift(suite);
    fn.call(suite);
    suites.shift(suite);
  }
}
複製代碼

每次處理一個 describe 時,咱們都會構建一個 Suite 實例來表示它,而且在執行 fn 前入棧,執行 fn 後出棧,保證 suites[0] 始終是當前正在處理的 suite 節點。利用這個棧列表,咱們能夠在遍歷過程當中構建出 suite 的樹級關係。

一樣的,其餘 API 也都須要依賴這個棧列表來實現:

// mocha/interfaces/bdd.js
module.exports = function (context, root) {
  // 記錄 suite 根節點到當前節點的路徑
  const suites = [root];

  // context.describe = ...

  context.it = context.specify = function (title, fn) {
    const parent = suites[0];
    const test = new Test({
      title,
      fn
    });
    parent.tests.push(test);
  }

  context.before = function (fn) {
    const cur = suites[0];
    cur._beforeAll.push(fn);
  }

  context.after = function (fn) {
    const cur = suites[0];
    cur._afterAll.push(fn);
  }

  context.beforeEach = function (fn) {
    const cur = suites[0];
    cur._beforeEach.push(fn);
  }

  context.afterEach = function (fn) {
    const cur = suites[0];
    cur._afterEach.push(fn);
  }
}
複製代碼
四、執行測試用例文件

一切準備就緒,咱們開始 require 測試用例文件。要完成這個步驟,咱們須要一個函數來協助完成,它負責解析 test 路徑下的資源,返回一個文件列表,而且可以支持 test 路徑爲文件和爲目錄的兩種狀況。

// mocha/src/utils.js
const path = require('path');
const fs = require('fs');

module.exports.lookupFiles = function lookupFiles(filepath) {
  let stat;

  // 假設路徑是文件
  try {
    stat = fs.statSync(`${filepath}.js`);
    if (stat.isFile()) {
      // 確實是文件,直接以數組形式返回
      return [filepath];
    }
  } catch(e) {}
	
  // 假設路徑是目錄
  let files = []; // 存放目錄下的全部文件
  fs.readdirSync(filepath).forEach(function(dirent) {
    let pathname = path.join(filepath, dirent);

    try {
      stat = fs.statSync(pathname);
      if (stat.isDirectory()) {
        // 是目錄,進一步遞歸
        files = files.concat(lookupFiles(pathname));
      } else if (stat.isFile()) {
        // 是文件,補充到待返回的文件列表中
        files.push(pathname);
      }
    } catch(e) {}
  });
	
  return files;
}
複製代碼
// mocha/src/mocha.js
// ...
const path = require('path');
const utils = require('./utils');

class Mocha {
  constructor() {
    // 建立一個根suite
    // ...
    // 使用bdd測試風格,將API掛載到global對象上
    // ...
    // 執行測試用例文件,構建suite-test樹
    const spec = path.resolve(__dirname, '../../test');
    const files = utils.lookupFiles(spec);
    files.forEach(file => {
      require(file);
    });
  }
  // ...
}
複製代碼

4、執行測試用例

在這個環節中,咱們須要經過遍歷 suite-test 樹來遞歸執行 suite 節點和 test 節點,並同步地輸出測試報告。

一、異步執行

Mocha 的測試用例和 Hook 函數是支持異步執行的。異步執行的寫法有兩種,一種是函數返回值爲一個 promise 對象,另外一種是函數接收一個入參 done,並由開發者在異步代碼中手動調用 done(error) 來向 Mocha 傳遞斷言結果。因此,在執行測試用例以前,咱們須要一個包裝函數,將開發者傳入的函數 promise 化:

// mocha/src/utils.js
// ...
module.exports.adaptPromise = function(fn) {
  return () => new Promise(resolve => {
    if (fn.length == 0) { // 不使用參數 done
      try {
        const ret = fn();
        // 判斷是否返回promise
        if (ret instanceof Promise) {
          return ret.then(resolve, resolve);
        } else {
          resolve();
        }
      } catch (error) {
        resolve(error);
      }
    } else { // 使用參數 done
      function done(error) {
        resolve(error);
      }
      fn(done);
    }
  })
}
複製代碼

這個工具函數傳入一個函數 fn 並返回另一個函數,執行返回的函數可以以 promise 的形式去運行 fn。這樣一來,咱們須要稍微修改一下以前的代碼:

// mocha/interfaces/bdd.js
// ...
const { adaptPromise } = require('../src/utils');

module.exports = function (context, root) {
  // ...
  context.it = context.specify = function (title, fn) {
    // ...
    const test = new Test({
      title,
      fn: adaptPromise(fn)
    });
    // ...
  }

  context.before = function (fn) {
    // ...
    cur._beforeAll.push(adaptPromise(fn));
  }

  context.after = function (fn) {
    // ...
    cur._afterAll.push(adaptPromise(fn));
  }

  context.beforeEach = function (fn) {
    // ...
    cur._beforeEach.push(adaptPromise(fn));
  }

  context.afterEach = function (fn) {
    // ...
    cur._afterEach.push(adaptPromise(fn));
  }
}
複製代碼
二、測試用例執行器

執行測試用例須要調度 suite 和 test 節點,所以咱們須要一個執行器(runner)來統一負責執行過程。這是執行階段的核心,咱們先直接貼代碼:

// mocha/src/runner.js
const EventEmitter = require('events').EventEmitter;

// 監聽事件的標識
const constants = {
  EVENT_RUN_BEGIN: 'EVENT_RUN_BEGIN',      // 執行流程開始
  EVENT_RUN_END: 'EVENT_RUN_END',          // 執行流程結束
  EVENT_SUITE_BEGIN: 'EVENT_SUITE_BEGIN',  // 執行suite開始
  EVENT_SUITE_END: 'EVENT_SUITE_END',      // 執行suite開始
  EVENT_FAIL: 'EVENT_FAIL',                // 執行用例失敗
  EVENT_PASS: 'EVENT_PASS'                 // 執行用例成功
}

class Runner extends EventEmitter {
  constructor() {
    super();
    // 記錄 suite 根節點到當前節點的路徑
    this.suites = [];
  }

  /* * 主入口 */
  async run(root) {
    this.emit(constants.EVENT_RUN_BEGIN);
    await this.runSuite(root);
    this.emit(constants.EVENT_RUN_END);
  }

  /* * 執行suite */
  async runSuite(suite) {
    // suite執行開始
    this.emit(constants.EVENT_SUITE_BEGIN, suite);

    // 1)執行before鉤子函數
    if (suite._beforeAll.length) {
      for (const fn of suite._beforeAll) {
        const result = await fn();
        if (result instanceof Error) {
          this.emit(constants.EVENT_FAIL, `"before all" hook in ${suite.title}: ${result.message}`);
          // suite執行結束
          this.emit(constants.EVENT_SUITE_END);
          return;
        }
      }
    }
  
    // 路徑棧推入當前節點
    this.suites.unshift(suite);
  
    // 2)執行test
    if (suite.tests.length) {
      for (const test of suite.tests) {
        await this.runTest(test);
      }
    }
  
    // 3)執行子級suite
    if (suite.suites.length) {
      for (const child of suite.suites) {
        await this.runSuite(child);
      }
    }
  
    // 路徑棧推出當前節點
    this.suites.shift(suite);
  
    // 4)執行after鉤子函數
    if (suite._afterAll.length) {
      for (const fn of suite._afterAll) {
        const result = await fn();
        if (result instanceof Error) {
          this.emit(constants.EVENT_FAIL, `"after all" hook in ${suite.title}: ${result.message}`);
          // suite執行結束
          this.emit(constants.EVENT_SUITE_END);
          return;
        }
      }
    }

    // suite結束
    this.emit(constants.EVENT_SUITE_END);
  }
  
  /* * 執行suite */
  async runTest(test) {
    // 1)由suite根節點向當前suite節點,依次執行beforeEach鉤子函數
    const _beforeEach = [].concat(this.suites).reverse().reduce((list, suite) => list.concat(suite._beforeEach), []);
    if (_beforeEach.length) {
      for (const fn of _beforeEach) {
        const result = await fn();
        if (result instanceof Error) {
          return this.emit(constants.EVENT_FAIL, `"before each" hook for ${test.title}: ${result.message}`)
        }
      }
    }
  
    // 2)執行測試用例
    const result = await test.fn();
    if (result instanceof Error) {
      return this.emit(constants.EVENT_FAIL, `${test.title}`);
    } else {
      this.emit(constants.EVENT_PASS, `${test.title}`);
    }
  
    // 3)由當前suite節點向suite根節點,依次執行afterEach鉤子函數
    const _afterEach = [].concat(this.suites).reduce((list, suite) => list.concat(suite._afterEach), []);
    if (_afterEach.length) {
      for (const fn of _afterEach) {
        const result = await fn();
        if (result instanceof Error) {
          return this.emit(constants.EVENT_FAIL, `"after each" hook for ${test.title}: ${result.message}`)
        }
      }
    }
  }
}

Runner.constants = constants;
module.exports = Runner
複製代碼

代碼很長,咱們稍微捋一下。

首先,咱們構造一個 Runner 類,利用兩個 async 方法來完成對 suite-test 樹的遍歷:

  • runSuite :負責執行 suite 節點。它不只須要調用 runTest 執行該 suite 節點上的若干 test 節點,還須要調用 runSuite 執行下一級的若干 suite 節點來實現遍歷,同時,before/after 也將在這裏獲得調用。執行順序依次是:before -> runTest -> runSuite -> after
  • runTest :負責執行 test 節點,主要是執行該 test 對象上定義的測試用例。另外,beforeEach/afterEach 的執行有一個相似瀏覽器事件捕獲和冒泡的過程,咱們須要沿節點路徑向當前 suite 節點方向和向 suite 根節點方向分別執行各 suite 的 beforeEach/afterEach 鉤子函數。執行順序依次是:beforeEach -> run test case -> afterEach

在遍歷過程當中,咱們依然是利用一個棧列表來維護 suite 根節點到當前節點的路徑。同時,這兩個流程都用 async/await 寫法來組織,保證全部任務在異步場景下依然是按序執行的。

其次,測試結論是「邊執行邊輸出」的。爲了在執行過程當中能向 reporter 實時通知執行結果和執行狀態,咱們讓 Runner 類繼承自 EventEmitter 類,使其具有訂閱/發佈事件的能力,這個後續會細講。

最後,咱們在 Mocha 實例的 run 方法中去實例化 Runner 並調用它:

// mocha/src/mocha.js
// ...
const Runner = require('./runner');

class Mocha {
  // ...
  run() {
    const runner = new Runner();
    runner.run(this.rootSuite);
  }
}
複製代碼
三、輸出測試報告

reporter 負責測試報告輸出,這個過程是在執行測試用例的過程當中同步進行的,所以咱們利用 EventEmitter 讓 reporter 和 runner 保持通訊。在 runner 中咱們已經在各個關鍵節點都做了 event emit,因此咱們只須要在 reporter 中加上相應的事件監聽便可:

// mocha/reporters/index.js
module.exports.spec = require('./spec');
複製代碼
// mocha/reporters/spec.js
const constants = require('../src/runner').constants;

module.exports = function (runner) {

  // 執行開始
  runner.on(constants.EVENT_RUN_BEGIN, function() {});

  // suite執行開始
  runner.on(constants.EVENT_SUITE_BEGIN, function(suite) {});

  // suite執行結束
  runner.on(constants.EVENT_SUITE_END, function() {});

  // 用例經過
  runner.on(constants.EVENT_PASS, function(title) {});

  // 用例失敗
  runner.on(constants.EVENT_FAIL, function(title) {});

  // 執行結束
  runner.once(constants.EVENT_RUN_END, function() {});
}
複製代碼

Mocha 類中引入 reporter,執行事件訂閱,就能讓 runner 將測試的狀態結果實時推送給 reporter 了:

// mocha/src/mocha.js
const reporters = require('../reporters');
// ...
class Mocha {
  // ...
  run() {
    const runner = new Runner();
    reporters['spec'](runner);
    runner.run(this.rootSuite);
  }
}
複製代碼

reporter 中能夠任意構造你想要的報告樣式輸出,例如這樣:

// mocha/reporters/spec.js
const constants = require('../src/runner').constants;

const colors = {
  pass: 90,
  fail: 31,
  green: 32,
}

function color(type, str) {
  return '\u001b[' + colors[type] + 'm' + str + '\u001b[0m';
}

module.exports = function (runner) {

  let indents = 0;
  let passes = 0;
  let failures = 0;

  function indent(i = 0) {
    return Array(indents + i).join(' ');
  }

  // 執行開始
  runner.on(constants.EVENT_RUN_BEGIN, function() {
    console.log();
  });

  // suite執行開始
  runner.on(constants.EVENT_SUITE_BEGIN, function(suite) {
    console.log();

    ++indents;
    console.log(indent(), suite.title);
  });

  // suite執行結束
  runner.on(constants.EVENT_SUITE_END, function() {
    --indents;
    if (indents == 1) console.log();
  });

  // 用例經過
  runner.on(constants.EVENT_PASS, function(title) {
    passes++;

    const fmt = indent(1) + color('green', ' ✓') + color('pass', ' %s');
    console.log(fmt, title);
  });

  // 用例失敗
  runner.on(constants.EVENT_FAIL, function(title) {
    failures++;

    const fmt = indent(1) + color('fail', ' × %s');
    console.log(fmt, title);
  });

  // 執行結束
  runner.once(constants.EVENT_RUN_END, function() {
    console.log(color('green', ' %d passing'), passes);
    console.log(color('fail', ' %d failing'), failures);
  });
}
複製代碼

5、驗證

到這裏,咱們的 simple-mocha 就基本完成了,咱們能夠編寫一個測試用例來簡單驗證一下:

// 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)))
    })
  })
})

describe('Srting', function () {
  describe('#replace', function () {
    it('should return a string that has been replaced', function () {
      assert.equal('hey Hankle', 'hey Densy'.replace('Densy', 'Hankle'))
    })
  })
})
複製代碼

這裏咱們用 node 內置的 assert 模塊來執行斷言測試。下邊是執行結果:

npm test

> simple-mocha@1.0.0 test /Documents/simple-mocha
> node mocha

   Array
     #indexOf()
        ✓ should return -1 when not present
        × should return the index when present
     #every()
        ✓ should return true when all items are satisfied

   String
     #replace
        ✓ should return a string that has been replaced

  3 passing
  1 failing
複製代碼

測試用例執行成功。附上完整的流程圖:

結尾

若是你看到了這裏,看完並看懂了上邊實現 simple-mocha 的整個流程,那麼很高興地告訴你,你已經掌握了 Mocha 最核心的運行機理。simple-mocha 的整個實現過程其實就是 Mocha 實現的一個簡化。而爲了讓你們在看完這篇文章後再去閱讀 Mocha 源碼時可以更快速地理解,我在簡化和淺化 Mocha 實現流程的同時,也儘量地保留了其中的一些命名和實現細節。有差異的地方,如執行測試用例環節,Mocha 源碼利用了一個複雜的 Hook 機制來實現異步測試的依序執行,而我爲了方便理解,用 async/await 來替代實現。固然這不是說 Mocha 實現得繁瑣,在更加複雜的測試場景下,這套 Hook 機制是十分必要的。因此,這篇文章僅僅但願可以幫助咱們攻克 Mocha 源碼閱讀的第一道陡坡,而要理解 Mocha 的精髓,光看這篇文章是遠遠不夠的,還得深刻閱讀 Mocha 源碼。

參考文章

Mocha官方文檔
BDD和Mocha框架


若是你以爲這篇內容對你有價值,請點贊,並關注咱們的官網和咱們的微信公衆號(WecTeam),每週都有優質文章推送:

WecTeam
相關文章
相關標籤/搜索