close
大廠技術堅持周更精選好文

本文為來自飛書 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.jsconsttoString=Object.prototype.toString;functiongetTag(value){if(value==null){returnvalue===undefined?'[objectUndefined]':'[objectNull]'}returntoString.call(value)}module.exports={getTag,};

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

//test/getTag.spec.jsconstassert=require('assert');const{getTag}=require('../index');describe('檢查:getTag函數執行',function(){before(function(){console.log('😁before鈎子觸發');});describe('測試:正常流',function(){it('類型返回:[object JSON]',function(done){setTimeout(()=>{assert.equal(getTag(JSON),'[objectJSON]');done();},1000);});it('類型返回:[object Number]',function(){assert.equal(getTag(1),'[objectNumber]');});});describe('測試:異常流',function(){it('類型返回:[object Undefined]',function(){assert.equal(getTag(undefined),'[objectUndefined]');});});after(function(){console.log('😭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),'[objectJSON]');done();},1000);});

這種異步代碼在我們實際業務中也是十分常見的,比如某一部分代碼依賴接口數據的返回,或是對某些定時器進行單測用例的編寫。mocha支持兩種方式的異步代碼,一種是回調函數直接返回一個Promise,一種是支持在回調函數中傳參數done,手動調用done函數來結束用例。

執行結果和執行順序
我們可以看到用例的執行是嚴格按照從外到里,從上到下的執行順序來執行,其中鈎子的執行順序和它的編寫順序無關,而且我們發現在測試用例編寫過程中,諸如describe、it和before/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類來控制整個流程的執行:
classMocha{constructor(){}run(){}}module.exports=Mocha;

入口文件更新為:

//mocha-demo/mocha/index.jsconstMocha=require('./src/mocha');constmocha=newMocha();mocha.run();

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

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

針對這段代碼結構如下:

image.png

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

//mocha/src/suite.jsclassSuite{/****@param{*}parent父節點*@param{*}titleSuite名稱,即describe傳入的第一個參數*/constructor(parent,title){this.title=title;//Suite名稱,即describe傳入的第一個參數this.parent=parent//父suitethis.suites=[];//子級suitethis.tests=[];//包含的it測試用例方法this._beforeAll=[];//before鈎子this._afterAll=[];//after鈎子this._beforeEach=[];//beforeEach鈎子this._afterEach=[];//afterEach鈎子//將當前Suite實例push到父級的suties數組中if(parentinstanceofSuite){parent.suites.push(this);}}}module.exports=Suite;

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

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

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

收集用例(通過Suite和Test類來構造整棵樹);
執行用例(遍歷這棵樹,執行所有的用例函數);

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

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

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

實現創建根節點
首先我們的測試用例樹要有個初始化根節點,在Mocha類中創建如下:
//mocha/src/mocha.jsconstSuite=require('./suite');classMocha{constructor(){//創建根節點this.rootSuite=newSuite(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'usestrict';exports.bdd=require('./bdd');

然後在Mocha類中進行bdd接口的全局掛載:

//mocha/src/mocha.jsconstinterfaces=require('../interfaces');classMocha{constructor(){//this.rootSuite=...//注意第二個參數是我們的前面創建的根節點,此時interfaces['bdd'](global,this.rootSuite"'bdd'");}run(){}}module.exports=Mocha;

此時我們已經完成了api的全局掛載,可以放心導入測試用例文件讓函數執行了。

導入測試用例文件
測試用例文件的導入mocha的實現比較複雜,支持配置,支持終端調用,也有支持CJS的實現,也有支持 ESM的實現,另外還有預加載,懶加載的實現,以滿足在不同場景下測試用例的執行時機。我們此處簡單的將測試用例文件的路徑寫死即可,直接加載我們本地使用的測試用例文件:
//mocha/src/utils.jsconstpath=require('path');constfs=require('fs');/****@param{*}filepath文件或是文件夾路徑*@returns所有測試文件路徑數組*/module.exports.findCaseFile=function(filepath){functionreadFileList(dir,fileList=[]){constfiles=fs.readdirSync(dir);files.forEach((item,_)=>{varfullPath=path.join(dir,item);conststat=fs.statSync(fullPath);if(stat.isDirectory()){readFileList(path.join(dir,item),fileList);//遞歸讀取文件}else{fileList.push(fullPath);}});returnfileList;}letfileList=[];//路徑如果是文件則直接返回try{conststat=fs.statSync(filepath);if(stat.isFile()){fileList=[filepath];returnfileList;}readFileList(filepath,fileList);}catch(e){console.log(e)}returnfileList;}

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

//mocha/src/mocha.jsconstpath=require('path');constinterfaces=require('../interfaces');constutils=require('./utils');classMocha{constructor(){//this.rootSuite=...//interfaces['bdd'](global,this.rootSuite"'bdd'");//寫死我們本地測試用例所在文件夾地址constspec=path.resolve(__dirname,'../../test');constfiles=utils.findCaseFile(spec);//加載測試用例文件files.forEach(file=>require(file));}run(){}}module.exports=Mocha;創建Suite-Test樹
到這一步我們的測試用例文件已經加載進來了,而describe和it函數也都已經執行,但我們上面的describe和it還都是個空函數,我們接下來修改下我們提供的describe和it函數,來創建我們需要的樹形結構,在前面我們已經在bdd.js文件中對describe和it進行了初始化,此時補充上我們借用棧創建Suite-Test樹的邏輯:
//mocha/interfaces/bdd.jsconstSuite=require('../src/suite');constTest=require('../src/test');module.exports=function(context,root){//樹的根節點進棧constsuites=[root];//context是describe的別名,主要目的是處於測試用例代碼的組織和可讀性的考慮context.describe=context.context=function(title,callback){//獲取當前棧中的當前節點constcur=suites[0];//實例化一個Suite對象,存儲當前的describe函數信息constsuite=newSuite(cur,title);//入棧suites.unshift(suite);//執行describe回調函數callback.call(suite);//Suite出棧suites.shift();}context.it=context.specify=function(title,fn){//獲取當前Suite節點constcur=suites[0];consttest=newTest(title,fn);//將Test實例對象存儲在tests數組中cur.tests.push(test);}//...}

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

image.png支持異步
前面說過,mocha支持異步代碼的用例編寫,異步代碼的支持也很簡單,我們可以在代碼內部實現一個Promise適配器,將所有的 測試用例 所在的回調函數包裹在適配器裡面,Promise適配器實現如下:
//mocha/src/utils.jsconstpath=require('path');constfs=require('fs');//module.exports.findCaseFile=...module.exports.adaptPromise=function(fn){return()=>newPromise(resolve=>{if(fn.length===0){//不使用參數donetry{constret=fn();//判斷是否返回promiseif(retinstanceofPromise){returnret.then(resolve,resolve);}else{resolve();}}catch(error){resolve(error);}}else{//使用參數donefunctiondone(error){resolve(error);}fn(done);}})}

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

//mocha/interfaces/bdd.jsconstSuite=require('../src/suite');constTest=require('../src/test');const{adaptPromise}=require('../src/utils');module.exports=function(context,root){constsuites=[root];//context是describe的別名,主要目的是處於測試用例代碼的組織和可讀性的考慮//context.describe=context.context=...context.it=context.specify=function(title,fn){constcur=suites[0];consttest=newTest(title,adaptPromise(fn));cur.tests.push(test);}context.before=function(fn){constcur=suites[0];cur._beforeAll.push(adaptPromise(fn));}context.after=function(fn){constcur=suites[0];cur._afterAll.push(adaptPromise(fn));}context.beforeEach=function(fn){constcur=suites[0];cur._beforeEach.push(adaptPromise(fn));}context.afterEach=function(fn){constcur=suites[0];cur._afterEach.push(adaptPromise(fn));}}執行測試用例
以上我們已經實現了所有收集測試用例的代碼,並且也支持了異步,對測試用例的執行比較複雜我們可以單獨創建一個Runner類去實現執行測試用例的邏輯:
//mocha/src/runner.jsclassRunner{}

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

image.png

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

//mocha/src/runner.jsclassRunner{constructor(){super();//記錄suite根節點到當前節點的路徑this.suites=[];}/**主入口*/asyncrun(root){//開始處理Suite節點awaitthis.runSuite(root);}/**處理suite*/asyncrunSuite(suite){//1.執行before鈎子函數if(suite._beforeAll.length){for(constfnofsuite._beforeAll){constresult=awaitfn();}}//推入當前節點this.suites.unshift(suite);//2.執行testif(suite.tests.length){for(consttestofsuite.tests){//執行test回調函數awaitthis.runTest(test);}}//3.執行子級suiteif(suite.suites.length){for(constchildofsuite.suites){//遞歸處理Suiteawaitthis.runSuite(child);}}//路徑棧推出節點this.suites.shift();//4.執行after鈎子函數if(suite._afterAll.length){for(constfnofsuite._afterAll){//執行回調constresult=awaitfn();}}}/**處理Test*/asyncrunTest(test){//1.由suite根節點向當前suite節點,依次執行beforeEach鈎子函數const_beforeEach=[].concat(this.suites).reverse().reduce((list,suite)=>list.concat(suite._beforeEach),[]);if(_beforeEach.length){for(constfnof_beforeEach){constresult=awaitfn();}}//2.執行測試用例constresult=awaittest.fn();//3.由當前suite節點向suite根節點,依次執行afterEach鈎子函數const_afterEach=[].concat(this.suites).reduce((list,suite)=>list.concat(suite._afterEach),[]);if(_afterEach.length){for(constfnof_afterEach){constresult=awaitfn();}}}}module.exports=Runner;

將Runner類注入到Mocha類中:

//mocha/src/mocha.jsconstRunner=require('./runner');classMocha{//constructor()..run(){construnner=newRunner();runner.run(this.rootSuite);}}module.exports=Mocha;

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

收集測試用例執行結果

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

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

//mocha/src/runner.jsconstEventEmitter=require('events').EventEmitter;//監聽事件的標識constconstants={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'//執行用例成功}classRunnerextendsEventEmitter{//.../**主入口*/asyncrun(root){this.emit(constants.EVENT_RUN_BEGIN);awaitthis.runSuite(root);this.emit(constants.EVENT_RUN_END);}/**執行suite*/asyncrunSuite(suite){//suite執行開始this.emit(constants.EVENT_SUITE_BEGIN,suite);//1.執行before鈎子函數if(suite._beforeAll.length){for(constfnofsuite._beforeAll){constresult=awaitfn();if(resultinstanceofError){this.emit(constants.EVENT_FAIL,`"beforeall"hookin${suite.title}:${result.message}`);//suite執行結束this.emit(constants.EVENT_SUITE_END);return;}}}//...//4.執行after鈎子函數if(suite._afterAll.length){for(constfnofsuite._afterAll){constresult=awaitfn();if(resultinstanceofError){this.emit(constants.EVENT_FAIL,`"afterall"hookin${suite.title}:${result.message}`);//suite執行結束this.emit(constants.EVENT_SUITE_END);return;}}}//suite結束this.emit(constants.EVENT_SUITE_END);}/**處理Test*/asyncrunTest(test){//1.由suite根節點向當前suite節點,依次執行beforeEach鈎子函數const_beforeEach=[].concat(this.suites).reverse().reduce((list,suite)=>list.concat(suite._beforeEach),[]);if(_beforeEach.length){for(constfnof_beforeEach){constresult=awaitfn();if(resultinstanceofError){returnthis.emit(constants.EVENT_FAIL,`"beforeeach"hookfor${test.title}:${result.message}`)}}}//2.執行測試用例constresult=awaittest.fn();if(resultinstanceofError){returnthis.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(constfnof_afterEach){constresult=awaitfn();if(resultinstanceofError){returnthis.emit(constants.EVENT_FAIL,`"aftereach"hookfor${test.title}:${result.message}`)}}}}}Runner.constants=constants;module.exports=Runner

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

//mocha/reporter/sped.jsconstconstants=require('../src/runner').constants;constcolors={pass:90,fail:31,green:32,}functioncolor(type,str){return'\u001b['+colors[type]+'m'+str+'\u001b[0m';}module.exports=function(runner){letindents=0;letpasses=0;letfailures=0;lettime=+newDate();functionindent(i=0){returnArray(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++;constfmt=indent(1)+color('green','✓')+color('pass','%s');console.log(fmt,title);});//用例失敗runner.on(constants.EVENT_FAIL,function(title){failures++;constfmt=indent(1)+color('fail','×%s');console.log(fmt,title);});//執行結束runner.once(constants.EVENT_RUN_END,function(){console.log(color('green','%dpassing'),passes,color('pass',`(${Date.now()-time}ms)`));console.log(color('fail','%dfailing'),failures);});}

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

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

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

constassert=require('assert');const{getTag}=require('../index');describe('檢查:getTag函數執行',function(){before(function(){console.log('😁before鈎子觸發');});describe('測試:正常流',function(){it('類型返回:[object JSON]',function(done){setTimeout(()=>{assert.equal(getTag(JSON),'[objectJSON]');done();},1000);});it('類型返回:[object Number]',function(){assert.equal(getTag(1),'[objectNumber]');});});describe('測試:異常流',function(){it('類型返回:[object Undefined]',function(){assert.equal(getTag(undefined),'[objectUndefined]');});it('類型返回:[object Object]',function(){assert.equal(getTag([]),'[objectObject]');});});after(function(){console.log('😭after鈎子觸發');});});

執行下:

一個精簡版mocha就此完成!

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

https://github.com/mochajs/mocha

https://mochajs.org/

參考資料
[1]

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

- END -
❤️ 謝謝支持

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

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

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

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

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()