深入理解 Mocha 測試框架:從零實現一個 Mocha

語言: CN / TW / HK

       

本文為來自飛書 aPaaS Growth 研發團隊成員的文章。

aPaaS Growth 團隊專注在使用者可感知的、巨集觀的 aPaaS 應用的搭建流程,及租戶、應用治理等產品路徑,致力於打造 aPaaS 平臺流暢的 “應用交付” 流程和體驗,完善應用構建相關的生態,加強應用搭建的便捷性和可靠性,提升應用的整體效能,從而助力 aPaaS 的使用者增長,與基礎團隊一起推進 aPaaS 在企業內外部的落地與提效。

前言

什麼是自動化測試

  • 自動化測試在很多團隊中都是Devops環節中很難執行起來的一個環節,主要原因在於測試程式碼的編寫工作很難抽象,99%的場景都需要和業務強繫結,而且寫測試程式碼的編寫工作量往往比編寫實際業務程式碼的工作量更多。在一些很多業務場景中投入產出比很低,適合寫自動化測試的應該是那些中長期業務以及一些諸如元件一樣的基礎庫。自動化測試是個比較大的概念,其中分類也比較多,比如單元測試,端對端測試,整合測試等等,其中單元測試相對而言是我們比較耳熟能詳的一個領域。單元測試框架有很多,比如Mocha,Jest,AVA等。Mocha是我們今天文章的重點,我們先來了解下mocha是怎樣的一款框架。

什麼是Mocha

  • Mocha是一款執行在nodejs上的測試框架,相信大家或多或少都有聽過或是見過,支援同步和非同步測試,同時還支援TDD,BDD等多種測試風格,mocha作為一款老牌的測試框架已經被廣泛應用在單元測試或是端對端測試的場景中。mocha的原始碼十分的冗長,而且包含了很多的高階玩法,但實際上mocha的核心原理是十分簡單的,導致原始碼體積龐雜的原因主要在於實現了很多其他的功能,做了很多程式碼上的相容處理。比如生成html格式的測試報告這種,支援多種的測試風格,外掛系統等等。但實際在業務中我們對mocha本身90%的場景的使用也僅僅是他的“測試”功能而已。諸如多種文字格式的測試覆蓋率報告的生成,斷言庫,測試資料mock等等其它功能都可以使用做的更好一些第三方庫來代替。mocha本身是個比較純粹的測試框架。

準備

瞭解mocha

  • 綜上所述,撇棄mocha其它的複雜實現,針對於它的核心原理的解讀是本次分享的主題。原始碼閱讀十分枯燥,我們將根據目前現有的mocha核心功能 實現一個簡易的mocha 。在此之前我們先認識下如何使用mocha,下面是一段來自lodash判斷資料型別的程式碼:

// 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,
};

上述程式碼使用了Object.prototype.toString來判斷了資料型別,我們針對上述程式碼的測試用例(此處斷言使用node原生的assert方法,採用BDD的測試風格):

// test/getTag.spec.js
const assert = require('assert');
const { getTag } = require('../index');

describe('檢查:getTag函式執行', function () {
before(function() {
console.log(':grin:before鉤子觸發');
});
describe('測試:正常流', function() {
it('型別返回: [object JSON]', function (done) {
setTimeout(() => {
assert.equal(getTag(JSON), '[object JSON]');
done();
}, 1000);
});
it('型別返回: [object Number]', function() {
assert.equal(getTag(1), '[object Number]');
});
});
describe('測試:異常流', function() {
it('型別返回: [object Undefined]', function() {
assert.equal(getTag(undefined), '[object Undefined]');
});
});
after(function() {
console.log(':sob:after鉤子觸發');
});
});

mocha提供的api語義還是比較強的,即使沒寫過單元測試程式碼,單看這段程式碼也不難理解這段程式碼幹了啥,而這段測試內碼表會作為我們最後驗證簡易Mocha的樣例,我們先來看下使用mocha執行該測試用例的執行結果:

如上圖所示,即我們前面測試程式碼的執行結果,我們來拆分下當前mocha實現的一些功能點。

注:mocha更多使用方法可參考 Mocha - the fun, simple, flexible JavaScript test framework [1]

核心函式

  • 首先我們可以看到mocha主要提供兩個 核心函式 describe it 來進行測試用例的編寫。 describe 函式我們稱之為 測試套件 ,它的核心功能是來描述測試的流程, it 函式我們稱之為一個 測試單元 ,它的功能是來執行具體的測試用例。

測試風格

  • 上面的測試用例編寫我們採用了典型的BDD風格,所謂的BDD風格可以理解為需求先行的一種測試風格,還有一種比較常見的測試風格TDD即測試驅動開發,TDD強調的是 測試先行 。在具體的業務開發中我們可以理解為TDD是指在寫具體的業務的程式碼之前先寫好測試用例,用提前編寫好的測試用例去一步步完善我們的業務程式碼,遵循著 測試用例->編碼 -> 驗證 -> 重構 的過程,而BDD是指標對既有的業務程式碼進行編寫測試用例,強調的是 行為先行 ,使得測試用例覆蓋業務程式碼所有的case。mocha預設採用的是BDD的測試風格,而且我們在實際開發中,更多涉及的其實也是BDD的測試風格,因此我們此次也將 實現BDD的測試風格

鉤子函式

  • 如上在執行測試套件或是測試單元之前mocha提供了很多的鉤子:

  • before:在執行測試套件之前觸發該鉤子;

  • after:在測試套件執行結束之後觸發該鉤子;

  • beforeEach:在每個測試單元執行之前觸發該鉤子;

  • afterEach:在每個測試單元執行結束後觸發該鉤子;

    • 鉤子的使用場景更多是在實際的業務場景中進行mock資料、測試資料收集、測試報告的自定義等;因此 鉤子也是mocha的核心功能之一

支援非同步

  • 如上第一個測試用例:

it('型別返回: [object JSON]', function (done) {
setTimeout(() => {
assert.equal(getTag(JSON), '[object JSON]');
done();
}, 1000);
});

這種非同步程式碼在我們實際業務中也是十分常見的,比如某一部分程式碼依賴介面資料的返回,或是對某些定時器進行單測用例的編寫。mocha支援兩種方式的非同步程式碼,一種是回撥函式直接 返回一個Promise ,一種是支援在回撥函式中傳引數done, 手動呼叫done函式 來結束用例。

執行結果和執行順序

  • 我們可以看到用例的執行是嚴格按照 從外到裡,從上到下 的執行順序來執行,其中鉤子的執行順序和它的編寫順序無關,而且我們發現在測試用例編寫過程中,諸如 describeitbefore/after 都無需引用依賴,直接呼叫即可,因此我們還要 實現下相關 api 的全域性掛載

設計

目錄結構設計

├── index.js            #待測試程式碼(業務程式碼)
├── mocha #簡易mocha所在目錄
│ ├── index.js #簡易mocha入口檔案
│ ├── interfaces #存放不同的測試風格
│ │ ├── bdd.js #BDD 測試風格的實現
│ │ └── index.js #方便不同測試風格的匯出
│ ├── reporters #生成測試報告
│ │ ├── index.js
│ │ └── spec.js
│ └── src #簡易mocha核心目錄
│ ├── mocha.js #存放Mocha類控制整個流程
│ ├── runner.js #Runner類,輔助Mocha類執行測試用例
│ ├── suite.js #Suite類,處理describe函式
│ ├── test.js #Test類,處理it函式
│ └── utils.js #存放一些工具函式
├── package.json
└── test #測試用例編寫
└── getTag.spec.js

上面的mocha資料夾就是我們將要實現的簡易版mocha目錄,目錄結構參考的mocha原始碼,但只採取了核心部分目錄結構。

總體流程設計

  • 首先我們需要一個整體的 Mocha類 來控制整個流程的執行:

class Mocha {
constructor() {}
run() {}
}
module.exports = Mocha;

入口檔案更新為:

// mocha-demo/mocha/index.js
const Mocha = require('./src/mocha');
const mocha = new Mocha();
mocha.run();

測試用例的執行過程順序尤其重要,前面說過用例的執行遵循從外到裡,從上到下的順序,對於 describeit 的回撥函式處理很容易讓我們想到這是一個樹形結構,而且是深度優先的遍歷順序。簡化下上面的用例程式碼:

describe('檢查:getTag函式執行', function () {
describe('測試:正常流', function() {
it('型別返回: [object JSON]', function (done) {
setTimeout(() => {
assert.equal(getTag(JSON), '[object JSON]');
done();
}, 1000);
});
it('型別返回: [object Number]', function() {
assert.equal(getTag(1), '[object Number]');
});
});
describe('測試:異常流', function() {
it('型別返回: [object Undefined]', function() {
assert.equal(getTag(undefined), '[object Undefined]');
});
});
});

針對這段程式碼結構如下:

image.png

整個樹的結構如上,而我們在處理具體的函式的時候則可以定義 Suite/Test兩個類 來分別描述 describe/it 兩個函式。可以看到describe函式是存在父子關係的,關於Suite類的屬性我們定義如下:

 // mocha/src/suite.js
class Suite {
/**
*
* @param { * } parent 父節點
* @param { * } title Suite名稱,即describe傳入的第一個引數
*/
constructor ( parent, title ) {
this . title = title; // Suite名稱,即describe傳入的第一個引數
this . parent = parent // 父suite
this . suites = []; // 子級suite
this . tests = []; // 包含的it 測試用例方法
this . _beforeAll = []; // before 鉤子
this . _afterAll = []; // after 鉤子
this . _beforeEach = []; // beforeEach鉤子
this . _afterEach = []; // afterEach 鉤子
// 將當前Suite例項push到父級的suties陣列中
if (parent instanceof Suite ) {
parent. suites . push ( this );
}
}
}

module . exports = Suite ;

而Test類代表it就可以定義的較為簡單:

 // mocha/src/test.js
class Test {
constructor(props) {
this.title = props.title; // Test名稱,it傳入的第一個引數
this.fn = props.fn; // Test的執行函式,it傳入的第二個引數
}
}

module.exports = Test;

此時我們整個流程就出來了:

  1. 收集用例(通過Suite和Test類來構造整棵樹);

  2. 執行用例(遍歷這棵樹,執行所有的用例函式);

  3. 收集測試用例的執行結果。

    1. 此時我們整個的流程如下(其中執行測試用例和收集執行結果已簡化):

image.png

OK,思路已經非常清晰,實現一下具體的程式碼吧

實現

建立根節點

  • 首先我們的測試用例樹要有個初始化根節點,在 Mocha類 中建立如下:

// mocha/src/mocha.js
const Suite = require('./suite');
class Mocha {
constructor() {
// 建立根節點
this.rootSuite = new Suite(null, '');
}
run() { }
}
module.exports = Mocha;

api全域性掛載

  • 實際上Mocha為BDD 測試風格提供了 describe()、context()、it()、specify()、before()、after()、beforeEach() 和 afterEach()共8個api,其中context僅僅是describe的別名,主要作用是為了保障測試用例編寫的可讀性和可維護性,與之類似specify則是it的別名。我們先將相關api初始化如下:

// mocha/interfaces/bdd.js
// context是我們的上下文環境,root是我們的樹的根節點
module.exports = function (context, root) {
// context是describe的別名,主要目的是處於測試用例程式碼的組織和可讀性的考慮
context.describe = context.context = function(title, fn) {}
// specify是it的別名
context.it = context.specify = function(title, fn) {}
context.before = function(fn) {}
context.after = function(fn) {}
context.beforeEach = function(fn) {}
context.afterEach = function(fn) {}
}

為方便支援各種測試風格介面我們進行統一的匯出:

// mocha/interfaces/index.js
'use strict';
exports.bdd = require('./bdd');

然後在Mocha類中進行bdd介面的全域性掛載:

// mocha/src/mocha.js
const interfaces = require('../interfaces');
class Mocha {
constructor() {
// this.rootSuite = ...
// 注意第二個引數是我們的前面建立的根節點,此時
interfaces['bdd'](global, this.rootSuite "'bdd'");
}
run() {}
}

module.exports = Mocha;

此時我們已經完成了api的全域性掛載,可以放心匯入測試用例檔案讓函式執行了。

匯入測試用例檔案

  • 測試用例檔案的匯入mocha的實現比較複雜,支援配置,支援終端呼叫,也有支援CJS的實現,也有支援 ESM的實現,另外還有預載入,懶載入的實現,以滿足在不同場景下測試用例的執行時機。我們此處簡單的將測試用例檔案的路徑寫死即可,直接載入我們本地使用的測試用例檔案:

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

/**
*
* @param { * } filepath 檔案或是資料夾路徑
* @returns 所有測試檔案路徑陣列
*/
module.exports.findCaseFile = function (filepath) {
function readFileList(dir, fileList = []) {
const files = fs.readdirSync(dir);
files.forEach((item, _ ) => {
var fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
readFileList(path.join(dir, item), fileList); // 遞迴讀取檔案
} else {
fileList.push(fullPath);
}
});
return fileList;
}
let fileList = [];
// 路徑如果是檔案則直接返回
try {
const stat = fs.statSync(filepath);
if (stat.isFile()) {
fileList = [filepath];
return fileList;
}
readFileList(filepath, fileList);
} catch(e) {console.log(e)}

return fileList;
}

上面函式簡單的實現了一個方法,用來遞迴的讀取本地所有的測試用例檔案,然後在Mocha類中使用該方法載入我們當前的測試用例檔案:

// mocha/src/mocha.js
const path = require('path');
const interfaces = require('../interfaces');
const utils = require('./utils');
class Mocha {
constructor() {
// this.rootSuite = ...
// interfaces['bdd'](global, this.rootSuite "'bdd'");
// 寫死我們本地測試用例所在資料夾地址
const spec = path.resolve(__dirname, '../../test');
const files = utils.findCaseFile(spec);
// 載入測試用例檔案
files.forEach(file => require(file));
}
run() {}
}

module.exports = Mocha;

建立Suite-Test樹

  • 到這一步我們的測試用例檔案已經載入進來了,而 describe和it函式也都已經執行 ,但我們上面的 describeit 還都是個空函式,我們接下來修改下我們提供的describe和it函式,來建立我們需要的樹形結構,在前面我們已經在bdd.js檔案中對describe和it進行了初始化,此時補充上我們 借用棧建立Suite-Test樹 的邏輯:
// mocha/interfaces/bdd.js

const Suite = require('../src/suite');
const Test = require('../src/test');

module.exports = function (context, root) {
// 樹的根節點進棧
const suites = [root];
// context是describe的別名,主要目的是處於測試用例程式碼的組織和可讀性的考慮
context.describe = context.context = function (title, callback) {
// 獲取當前棧中的當前節點
const cur = suites[0];
// 例項化一個Suite物件,儲存當前的describe函式資訊
const suite = new Suite(cur, title);
// 入棧
suites.unshift(suite);
// 執行describe回撥函式
callback.call(suite);
// Suite出棧
suites.shift();
}
context.it = context.specify = function (title, fn) {
// 獲取當前Suite節點
const cur = suites[0];
const test = new Test(title, fn);
// 將Test例項物件儲存在tests陣列中
cur.tests.push(test);
}
// ...
}

注意,上面的程式碼我們僅僅是通過執行describe的回撥函式將樹的結構建立了出來,裡面具體的測試用例程式碼(it的回撥函式) 還未開始執行 。基於以上程式碼,我們整個Suite-Test樹就已經創建出來了,截止到目前的程式碼我們 收集用例 的過程已經實現完成。此時我們的Sute-Test樹創建出來是這樣的結構:

image.png

支援非同步

  • 前面說過,mocha支援非同步程式碼的用例編寫,非同步程式碼的支援也很簡單,我們可以在程式碼內部實現一個Promise介面卡, 將所有的 測試用例 所在的回撥函式包裹在介面卡裡面 ,Promise介面卡實現如下:

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

// module.exports.findCaseFile = ...

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

我們改造下之前建立的Suite-Test樹,將it、before、after、beforeEach和afterEach的回撥函式進行適配:

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

module.exports = function (context, root) {
const suites = [root];
// context是describe的別名,主要目的是處於測試用例程式碼的組織和可讀性的考慮
// context.describe = context.context = ...
context.it = context.specify = function (title, fn) {
const cur = suites[0];
const test = new Test(title, adaptPromise(fn));
cur.tests.push(test);
}
context.before = function (fn) {
const cur = suites[0];
cur._beforeAll.push(adaptPromise(fn));
}
context.after = function (fn) {
const cur = suites[0];
cur._afterAll.push(adaptPromise(fn));
}
context.beforeEach = function (fn) {
const cur = suites[0];
cur._beforeEach.push(adaptPromise(fn));
}
context.afterEach = function (fn) {
const cur = suites[0];
cur._afterEach.push(adaptPromise(fn));
}
}

執行測試用例

  • 以上我們已經實現了所有收集測試用例的程式碼,並且也支援了非同步,對測試用例的執行比較複雜我們可以單獨建立一個 Runner類 去實現執行測試用例的邏輯:

// mocha/src/runner.js
class Runner {}

此時梳理下測試用例的執行邏輯,基於以上建立的Suite-Test樹,我們可以對樹進行一個遍歷從而執行所有的測試用例,而對於非同步程式碼的執行我們可以借用 async/await 來實現。此時我們的流程圖更新如下:

image.png

整個思路梳理下來就很簡單了,針對Suite-Test樹,從根節點開始遍歷這棵樹,將這棵樹中所有的Test節點所掛載的回撥函式進行執行即可。相關程式碼實現如下:

// mocha/src/runner.js
class Runner {
constructor() {
super();
// 記錄 suite 根節點到當前節點的路徑
this.suites = [];
}
/*
* 主入口
*/
async run(root) {
// 開始處理Suite節點
await this.runSuite(root);
}
/*
* 處理suite
*/
async runSuite(suite) {
// 1.執行before鉤子函式
if (suite._beforeAll.length) {
for (const fn of suite._beforeAll) {
const result = await fn();
}
}
// 推入當前節點
this.suites.unshift(suite);

// 2. 執行test
if (suite.tests.length) {
for (const test of suite.tests) {
// 執行test回撥函式
await this.runTest(test);
}
}

// 3. 執行子級suite
if (suite.suites.length) {
for (const child of suite.suites) {
// 遞迴處理Suite
await this.runSuite(child);
}
}

// 路徑棧推出節點
this.suites.shift();

// 4.執行after鉤子函式
if (suite._afterAll.length) {
for (const fn of suite._afterAll) {
// 執行回撥
const result = await fn();
}
}
}

/*
* 處理Test
*/
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();
}
}
// 2. 執行測試用例
const result = await test.fn();
// 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();
}
}
}
}
module.exports = Runner;

將Runner類注入到Mocha類中:

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

class Mocha {
// constructor()..
run() {
const runner = new Runner();
runner.run(this.rootSuite);
}
}

module.exports = Mocha;

簡單介紹下上面的程式碼邏輯,Runner類包括兩個方法,一個方法用來處理Suite,一個方法用來處理Test,使用棧的結構遍歷Suite-Test樹, 遞迴處理所有的Suite節點 ,從而找到所有的Test節點, 將Test中的回撥函式進行處理 ,測試用例執行結束。但到這裡我們會發現,只是執行了測試用例而已,測試用例的執行結果還沒獲取到,測試用例哪個通過了,哪個沒通過我們也無法得知。

收集測試用例執行結果

我們需要一箇中間人來記錄下執行的結果,輸出給我們,此時我們的流程圖更新如下:

修改Runner類,讓它繼承EventEmitter,來實現事件的傳遞工作:

// 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 {
// ...
/*
* 主入口
*/
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;
}
}
}

// ...

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

/*
* 處理Test
*/
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

在測試結果的處理函式中監聽執行結果的回撥進行統一處理:

// mocha/reporter/sped.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;
let time = +new Date();
function indent(i = 0) {
return Array(indents + i).join(' ');
}
// 執行開始
runner.on(constants.EVENT_RUN_BEGIN, function() {});
// suite執行開始
runner.on(constants.EVENT_SUITE_BEGIN, function(suite) {
++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, color('pass', `(${Date.now() - time}ms)`));
console.log(color('fail', ' %d failing'), failures);
});
}

上面程式碼的作用對程式碼進行了收集。

驗證

  • 截止到目前我們實現的mocha已經完成,執行下npm test看下用例的執行結果。

我們再手動構造一個失敗用例:

const assert = require('assert');
const { getTag } = require('../index');
describe('檢查:getTag函式執行', function () {
before(function() {
console.log(':grin:before鉤子觸發');
});
describe('測試:正常流', function() {
it('型別返回: [object JSON]', function (done) {
setTimeout(() => {
assert.equal(getTag(JSON), '[object JSON]');
done();
}, 1000);
});
it('型別返回: [object Number]', function() {
assert.equal(getTag(1), '[object Number]');
});
});
describe('測試:異常流', function() {
it('型別返回: [object Undefined]', function() {
assert.equal(getTag(undefined), '[object Undefined]');
});
it('型別返回: [object Object]', function() {
assert.equal(getTag([]), '[object Object]');
});
});
after(function() {
console.log(':sob:after鉤子觸發');
});
});

執行下:

一個精簡版mocha就此完成!

後記

  • 整個mocha的核心思想還是十分簡單的,但mocha的強大遠不止此,mocha是個非常靈活的測試框架,可擴充套件性很高,但也與此同時會帶來一些學習成本。像Jest那種包攬一切,斷言庫,快照測試,資料mock,測試覆蓋率報告的生成等等全部打包提供的使用起來是很方便,但問題在於不方便去做一些定製化開發。而mocha搭配他的生態(用chai斷言,用sinon來mock資料,istanbul來生成覆蓋率報告等)可以很方便的去做一些定製化開發。

參考

http://github.com/mochajs/mocha

http://mochajs.org/

參考資料

[1]

Mocha - the fun, simple, flexible JavaScript test framework: http://mochajs.org/

- END -

:heart: 謝謝支援

以上便是本次分享的全部內容,希望對你有所幫助^_^

喜歡的話別忘了 分享、點贊、收藏 三連哦~。

歡迎關注公眾號 ELab團隊 收貨大廠一手好文章~

aPaaS Growth 團隊專注在使用者可感知的、巨集觀的 aPaaS 應用的搭建流程,及租戶、應用治理等產品路徑,致力於打造 aPaaS 平臺流暢的 “應用交付” 流程和體驗,完善應用構建相關的生態,加強應用搭建的便捷性和可靠性,提升應用的整體效能,從而助力 aPaaS 的使用者增長,與基礎團隊一起推進 aPaaS 在企業內外部的落地與提效。