摘要:在真正寫(xiě)了一段時(shí)間的基礎(chǔ)組件和基礎(chǔ)工具后,才發(fā)現(xiàn)自動(dòng)化測(cè)試有很多好處。有了自動(dòng)化測(cè)試,開(kāi)發(fā)者會(huì)更加信任自己的代碼。由于維護(hù)測(cè)試用例也是一大筆開(kāi)銷(xiāo)畢竟沒(méi)有多少測(cè)試會(huì)專(zhuān)門(mén)幫前端寫(xiě)業(yè)務(wù)測(cè)試用例,而前端使用的流程自動(dòng)化工具更是沒(méi)有測(cè)試參與了。
前言 為何要測(cè)試本文轉(zhuǎn)載自 天貓前端博客,更多精彩文章請(qǐng)進(jìn)入天貓前端博客查看
以前不喜歡寫(xiě)測(cè)試,主要是覺(jué)得編寫(xiě)和維護(hù)測(cè)試用例非常的浪費(fèi)時(shí)間。在真正寫(xiě)了一段時(shí)間的基礎(chǔ)組件和基礎(chǔ)工具后,才發(fā)現(xiàn)自動(dòng)化測(cè)試有很多好處。測(cè)試最重要的自然是提升代碼質(zhì)量。代碼有測(cè)試用例,雖不能說(shuō)百分百無(wú)bug,但至少說(shuō)明測(cè)試用例覆蓋到的場(chǎng)景是沒(méi)有問(wèn)題的。有測(cè)試用例,發(fā)布前跑一下,可以杜絕各種疏忽而引起的功能bug。
自動(dòng)化測(cè)試另外一個(gè)重要特點(diǎn)就是快速反饋,反饋越迅速意味著開(kāi)發(fā)效率越高。拿UI組件為例,開(kāi)發(fā)過(guò)程都是打開(kāi)瀏覽器刷新頁(yè)面點(diǎn)點(diǎn)點(diǎn)才能確定UI組件工作情況是否符合自己預(yù)期。接入自動(dòng)化測(cè)試以后,通過(guò)腳本代替這些手動(dòng)點(diǎn)擊,接入代碼watch后每次保存文件都能快速得知自己的的改動(dòng)是否影響功能,節(jié)省了很多時(shí)間,畢竟機(jī)器干事情比人總是要快得多。
有了自動(dòng)化測(cè)試,開(kāi)發(fā)者會(huì)更加信任自己的代碼。開(kāi)發(fā)者再也不會(huì)懼怕將代碼交給別人維護(hù),不用擔(dān)心別的開(kāi)發(fā)者在代碼里搞“破壞”。后人接手一段有測(cè)試用例的代碼,修改起來(lái)也會(huì)更加從容。測(cè)試用例里非常清楚的闡釋了開(kāi)發(fā)者和使用者對(duì)于這端代碼的期望和要求,也非常有利于代碼的傳承。
考慮投入產(chǎn)出比來(lái)做測(cè)試說(shuō)了這么多測(cè)試的好處,并不代表一上來(lái)就要寫(xiě)出100%場(chǎng)景覆蓋的測(cè)試用例。個(gè)人一直堅(jiān)持一個(gè)觀點(diǎn):基于投入產(chǎn)出比來(lái)做測(cè)試。由于維護(hù)測(cè)試用例也是一大筆開(kāi)銷(xiāo)(畢竟沒(méi)有多少測(cè)試會(huì)專(zhuān)門(mén)幫前端寫(xiě)業(yè)務(wù)測(cè)試用例,而前端使用的流程自動(dòng)化工具更是沒(méi)有測(cè)試參與了)。對(duì)于像基礎(chǔ)組件、基礎(chǔ)模型之類(lèi)的不常變更且復(fù)用較多的部分,可以考慮去寫(xiě)測(cè)試用例來(lái)保證質(zhì)量。個(gè)人比較傾向于先寫(xiě)少量的測(cè)試用例覆蓋到80%+的場(chǎng)景,保證覆蓋主要使用流程。一些極端場(chǎng)景出現(xiàn)的bug可以在迭代中形成測(cè)試用例沉淀,場(chǎng)景覆蓋也將逐漸趨近100%。但對(duì)于迭代較快的業(yè)務(wù)邏輯以及生存時(shí)間不長(zhǎng)的活動(dòng)頁(yè)面之類(lèi)的就別花時(shí)間寫(xiě)測(cè)試用例了,維護(hù)測(cè)試用例的時(shí)間大了去了,成本太高。
Node.js模塊的測(cè)試對(duì)于Node.js的模塊,測(cè)試算是比較方便的,畢竟源碼和依賴(lài)都在本地,看得見(jiàn)摸得著。
測(cè)試工具測(cè)試主要使用到的工具是測(cè)試框架、斷言庫(kù)以及代碼覆蓋率工具:
測(cè)試框架:Mocha、Jasmine等等,測(cè)試主要提供了清晰簡(jiǎn)明的語(yǔ)法來(lái)描述測(cè)試用例,以及對(duì)測(cè)試用例分組,測(cè)試框架會(huì)抓取到代碼拋出的AssertionError,并增加一大堆附加信息,比如那個(gè)用例掛了,為什么掛等等。測(cè)試框架通常提供TDD(測(cè)試驅(qū)動(dòng)開(kāi)發(fā))或BDD(行為驅(qū)動(dòng)開(kāi)發(fā))的測(cè)試語(yǔ)法來(lái)編寫(xiě)測(cè)試用例,關(guān)于TDD和BDD的對(duì)比可以看一篇比較知名的文章The Difference Between TDD and BDD。不同的測(cè)試框架支持不同的測(cè)試語(yǔ)法,比如Mocha既支持TDD也支持BDD,而Jasmine只支持BDD。這里后續(xù)以Mocha的BDD語(yǔ)法為例
斷言庫(kù):Should.js、chai、expect.js等等,斷言庫(kù)提供了很多語(yǔ)義化的方法來(lái)對(duì)值做各種各樣的判斷。當(dāng)然也可以不用斷言庫(kù),Node.js中也可以直接使用原生assert庫(kù)。這里后續(xù)以Should.js為例
代碼覆蓋率:istanbul等等為代碼在語(yǔ)法級(jí)分支上打點(diǎn),運(yùn)行了打點(diǎn)后的代碼,根據(jù)運(yùn)行結(jié)束后收集到的信息和打點(diǎn)時(shí)的信息來(lái)統(tǒng)計(jì)出當(dāng)前測(cè)試用例的對(duì)源碼的覆蓋情況。
一個(gè)煎蛋的栗子以如下的Node.js項(xiàng)目結(jié)構(gòu)為例
. ├── LICENSE ├── README.md ├── index.js ├── node_modules ├── package.json └── test └── test.js
首先自然是安裝工具,這里先裝測(cè)試框架和斷言庫(kù):npm install --save-dev mocha should。裝完后就可以開(kāi)始測(cè)試之旅了。
比如當(dāng)前有一段js代碼,放在index.js里
"use strict"; module.exports = () => "Hello Tmall";
那么對(duì)于這么一個(gè)函數(shù),首先需要定一個(gè)測(cè)試用例,這里很明顯,運(yùn)行函數(shù),得到字符串Hello Tmall就算測(cè)試通過(guò)。那么就可以按照Mocha的寫(xiě)法來(lái)寫(xiě)一個(gè)測(cè)試用例,因此新建一個(gè)測(cè)試代碼在test/index.js
"use strict"; require("should"); const mylib = require("../index"); describe("My First Test", () => { it("should get "Hello Tmall"", () => { mylib().should.be.eql("Hello Tmall"); }); });
測(cè)試用例寫(xiě)完了,那么怎么知道測(cè)試結(jié)果呢?
由于我們之前已經(jīng)安裝了Mocha,可以在node_modules里面找到它,Mocha提供了命令行工具_(dá)mocha,可以直接在./node_modules/.bin/_mocha找到它,運(yùn)行它就可以執(zhí)行測(cè)試了:
這樣就可以看到測(cè)試結(jié)果了。同樣我們可以故意讓測(cè)試不通過(guò),修改test.js代碼為:
"use strict"; require("should"); const mylib = require("../index"); describe("My First Test", () => { it("should get "Hello Taobao"", () => { mylib().should.be.eql("Hello Taobao"); }); });
就可以看到下圖了:
Mocha實(shí)際上支持很多參數(shù)來(lái)提供很多靈活的控制,比如使用./node_modules/.bin/_mocha --require should,Mocha在啟動(dòng)測(cè)試時(shí)就會(huì)自己去加載Should.js,這樣test/test.js里就不需要手動(dòng)require("should");了。更多參數(shù)配置可以查閱Mocha官方文檔。
那么這些測(cè)試代碼分別是啥意思呢?
這里首先引入了斷言庫(kù)Should.js,然后引入了自己的代碼,這里it()函數(shù)定義了一個(gè)測(cè)試用例,通過(guò)Should.js提供的api,可以非常語(yǔ)義化的描述測(cè)試用例。那么describe又是干什么的呢?
describe干的事情就是給測(cè)試用例分組。為了盡可能多的覆蓋各種情況,測(cè)試用例往往會(huì)有很多。這時(shí)候通過(guò)分組就可以比較方便的管理(這里提一句,describe是可以嵌套的,也就是說(shuō)外層分組了之后,內(nèi)部還可以分子組)。另外還有一個(gè)非常重要的特性,就是每個(gè)分組都可以進(jìn)行預(yù)處理(before、beforeEach)和后處理(after, afterEach)。
如果把index.js源碼改為:
"use strict"; module.exports = bu => `Hello ${bu}`;
為了測(cè)試不同的bu,測(cè)試用例也對(duì)應(yīng)的改為:
"use strict"; require("should"); const mylib = require("../index"); let bu = "none"; describe("My First Test", () => { describe("Welcome to Tmall", () => { before(() => bu = "Tmall"); after(() => bu = "none"); it("should get "Hello Tmall"", () => { mylib(bu).should.be.eql("Hello Tmall"); }); }); describe("Welcome to Taobao", () => { before(() => bu = "Taobao"); after(() => bu = "none"); it("should get "Hello Taobao"", () => { mylib(bu).should.be.eql("Hello Taobao"); }); }); });
同樣運(yùn)行一下./node_modules/.bin/_mocha就可以看到如下圖:
這里before會(huì)在每個(gè)分組的所有測(cè)試用例運(yùn)行前,相對(duì)的after則會(huì)在所有測(cè)試用例運(yùn)行后執(zhí)行,如果要以測(cè)試用例為粒度,可以使用beforeEach和afterEach,這兩個(gè)鉤子則會(huì)分別在該分組每個(gè)測(cè)試用例運(yùn)行前和運(yùn)行后執(zhí)行。由于很多代碼都需要模擬環(huán)境,可以再這些before或beforeEach做這些準(zhǔn)備工作,然后在after或afterEach里做回收操作。
異步代碼的測(cè)試 回調(diào)這里很顯然代碼都是同步的,但很多情況下我們的代碼都是異步執(zhí)行的,那么異步的代碼要怎么測(cè)試呢?
比如這里index.js的代碼變成了一段異步代碼:
"use strict"; module.exports = (bu, callback) => process.nextTick(() => callback(`Hello ${bu}`));
由于源代碼變成異步,所以測(cè)試用例就得做改造:
"use strict"; require("should"); const mylib = require("../index"); describe("My First Test", () => { it("Welcome to Tmall", done => { mylib("Tmall", rst => { rst.should.be.eql("Hello Tmall"); done(); }); }); });
這里傳入it的第二個(gè)參數(shù)的函數(shù)新增了一個(gè)done參數(shù),當(dāng)有這個(gè)參數(shù)時(shí),這個(gè)測(cè)試用例會(huì)被認(rèn)為是異步測(cè)試,只有在done()執(zhí)行時(shí),才認(rèn)為測(cè)試結(jié)束。那如果done()一直沒(méi)有執(zhí)行呢?Mocha會(huì)觸發(fā)自己的超時(shí)機(jī)制,超過(guò)一定時(shí)間(默認(rèn)是2s,時(shí)長(zhǎng)可以通過(guò)--timeout參數(shù)設(shè)置)就會(huì)自動(dòng)終止測(cè)試,并以測(cè)試失敗處理。
當(dāng)然,before、beforeEach、after、afterEach這些鉤子,同樣支持異步,使用方式和it一樣,在傳入的函數(shù)第一個(gè)參數(shù)加上done,然后在執(zhí)行完成后執(zhí)行即可。
Promise平常我們直接寫(xiě)回調(diào)會(huì)感覺(jué)自己很low,也容易出現(xiàn)回調(diào)金字塔,我們可以使用Promise來(lái)做異步控制,那么對(duì)于Promise控制下的異步代碼,我們要怎么測(cè)試呢?
首先把源碼做點(diǎn)改造,返回一個(gè)Promise對(duì)象:
"use strict"; module.exports = bu => new Promise(resolve => resolve(`Hello ${bu}`));
當(dāng)然,如果是co黨也可以直接使用co包裹:
"use strict"; const co = require("co"); module.exports = co.wrap(function* (bu) { return `Hello ${bu}`; });
對(duì)應(yīng)的修改測(cè)試用例如下:
"use strict"; require("should"); const mylib = require("../index"); describe("My First Test", () => { it("Welcome to Tmall", () => { return mylib("Tmall").should.be.fulfilledWith("Hello Tmall"); }); });
Should.js在8.x.x版本自帶了Promise支持,可以直接使用fullfilled()、rejected()、fullfilledWith()、rejectedWith()等等一系列API測(cè)試Promise對(duì)象。
異步運(yùn)行測(cè)試注意:使用should測(cè)試Promise對(duì)象時(shí),請(qǐng)一定要return,一定要return,一定要return,否則斷言將無(wú)效
有時(shí)候,我們可能并不只是某個(gè)測(cè)試用例需要異步,而是整個(gè)測(cè)試過(guò)程都需要異步執(zhí)行。比如測(cè)試Gulp插件的一個(gè)方案就是,首先運(yùn)行Gulp任務(wù),完成后測(cè)試生成的文件是否和預(yù)期的一致。那么如何異步執(zhí)行整個(gè)測(cè)試過(guò)程呢?
其實(shí)Mocha提供了異步啟動(dòng)測(cè)試,只需要在啟動(dòng)Mocha的命令后加上--delay參數(shù),Mocha就會(huì)以異步方式啟動(dòng)。這種情況下我們需要告訴Mocha什么時(shí)候開(kāi)始跑測(cè)試用例,只需要執(zhí)行run()方法即可。把剛才的test/test.js修改成下面這樣:
"use strict"; require("should"); const mylib = require("../index"); setTimeout(() => { describe("My First Test", () => { it("Welcome to Tmall", () => { return mylib("Tmall").should.be.fulfilledWith("Hello Tmall"); }); }); run(); }, 1000);
直接執(zhí)行./node_modules/.bin/_mocha就會(huì)發(fā)生下面這樣的杯具:
那么加上--delay試試:
熟悉的綠色又回來(lái)了!
代碼覆蓋率單元測(cè)試玩得差不多了,可以開(kāi)始試試代碼覆蓋率了。首先需要安裝代碼覆蓋率工具istanbul:npm install --save-dev istanbul,istanbul同樣有命令行工具,在./node_modules/.bin/istanbul可以尋覓到它的身影。Node.js端做代碼覆蓋率測(cè)試很簡(jiǎn)單,只需要用istanbul啟動(dòng)Mocha即可,比如上面那個(gè)測(cè)試用例,運(yùn)行./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- --delay,可以看到下圖:
這就是代碼覆蓋率結(jié)果了,因?yàn)閕ndex.js中的代碼比較簡(jiǎn)單,所以直接就100%了,那么修改一下源碼,加個(gè)if吧:
"use strict"; module.exports = bu => new Promise(resolve => { if (bu === "Tmall") return resolve(`Welcome to Tmall`); resolve(`Hello ${bu}`); });
測(cè)試用例也跟著變一下:
"use strict"; require("should"); const mylib = require("../index"); setTimeout(() => { describe("My First Test", () => { it("Welcome to Tmall", () => { return mylib("Tmall").should.be.fulfilledWith("Welcome to Tmall"); }); }); run(); }, 1000);
換了姿勢(shì),我們?cè)賮?lái)一次./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- --delay,可以得到下圖:
當(dāng)使用istanbul運(yùn)行Mocha時(shí),istanbul命令自己的參數(shù)放在--之前,需要傳遞給Mocha的參數(shù)放在--之后
如預(yù)期所想,覆蓋率不再是100%了,這時(shí)候我想看看哪些代碼被運(yùn)行了,哪些沒(méi)有,怎么辦呢?
運(yùn)行完成后,項(xiàng)目下會(huì)多出一個(gè)coverage文件夾,這里就是放代碼覆蓋率結(jié)果的地方,它的結(jié)構(gòu)大致如下:
. ├── coverage.json ├── lcov-report │?? ├── base.css │?? ├── index.html │?? ├── prettify.css │?? ├── prettify.js │?? ├── sort-arrow-sprite.png │?? ├── sorter.js │?? └── test │?? ├── index.html │?? └── index.js.html └── lcov.info
coverage.json和lcov.info:測(cè)試結(jié)果描述的json文件,這個(gè)文件可以被一些工具讀取,生成可視化的代碼覆蓋率結(jié)果,這個(gè)文件后面接入持續(xù)集成時(shí)還會(huì)提到。
lcov-report:通過(guò)上面兩個(gè)文件由工具處理后生成的覆蓋率結(jié)果頁(yè)面,打開(kāi)可以非常直觀的看到代碼的覆蓋率
這里open coverage/lcov-report/index.html可以看到文件目錄,點(diǎn)擊對(duì)應(yīng)的文件進(jìn)入到文件詳情,可以看到index.js的覆蓋率如圖所示:
這里有四個(gè)指標(biāo),通過(guò)這些指標(biāo),可以量化代碼覆蓋情況:
statements:可執(zhí)行語(yǔ)句執(zhí)行情況
branches:分支執(zhí)行情況,比如if就會(huì)產(chǎn)生兩個(gè)分支,我們只運(yùn)行了其中的一個(gè)
Functions:函數(shù)執(zhí)行情況
Lines:行執(zhí)行情況
下面代碼部分,沒(méi)有被執(zhí)行過(guò)得代碼會(huì)被標(biāo)紅,這些標(biāo)紅的代碼往往是bug滋生的土壤,我們要盡可能消除這些紅色。為此我們添加一個(gè)測(cè)試用例:
"use strict"; require("should"); const mylib = require("../index"); setTimeout(() => { describe("My First Test", () => { it("Welcome to Tmall", () => { return mylib("Tmall").should.be.fulfilledWith("Welcome to Tmall"); }); it("Hello Taobao", () => { return mylib("Taobao").should.be.fulfilledWith("Hello Taobao"); }); }); run(); }, 1000);
再來(lái)一次./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- --delay,重新打開(kāi)覆蓋率頁(yè)面,可以看到紅色已經(jīng)消失了,覆蓋率100%。目標(biāo)完成,可以睡個(gè)安穩(wěn)覺(jué)了
集成到package.json好了,一個(gè)簡(jiǎn)單的Node.js測(cè)試算是做完了,這些測(cè)試任務(wù)都可以集中寫(xiě)到package.json的scripts字段中,比如:
{ "scripts": { "test": "NODE_ENV=test ./node_modules/.bin/_mocha --require should", "cov": "NODE_ENV=test ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- --delay" }, }
這樣直接運(yùn)行npm run test就可以跑單元測(cè)試,運(yùn)行npm run cov就可以跑代碼覆蓋率測(cè)試了,方便快捷
對(duì)多個(gè)文件分別做測(cè)試通常我們的項(xiàng)目都會(huì)有很多文件,比較推薦的方法是對(duì)每個(gè)文件多帶帶去做測(cè)試。比如代碼在./lib/下,那么./lib/文件夾下的每個(gè)文件都應(yīng)該對(duì)應(yīng)一個(gè)./test/文件夾下的文件名_spec.js的測(cè)試文件
為什么要這樣呢?不能直接運(yùn)行index.js入口文件做測(cè)試嗎?
直接從入口文件來(lái)測(cè)其實(shí)是黑盒測(cè)試,我們并不知道代碼內(nèi)部運(yùn)行情況,只是看某個(gè)特定的輸入能否得到期望的輸出。這通??梢愿采w到一些主要場(chǎng)景,但是在代碼內(nèi)部的一些邊緣場(chǎng)景,就很難直接通過(guò)從入口輸入特定的數(shù)據(jù)來(lái)解決了。比如代碼里需要發(fā)送一個(gè)請(qǐng)求,入口只是傳入一個(gè)url,url本身正確與否只是一個(gè)方面,當(dāng)時(shí)的網(wǎng)絡(luò)狀況和服務(wù)器狀況是無(wú)法預(yù)知的。傳入相同的url,可能由于服務(wù)器掛了,也可能因?yàn)榫W(wǎng)絡(luò)抖動(dòng),導(dǎo)致請(qǐng)求失敗而拋出錯(cuò)誤,如果這個(gè)錯(cuò)誤沒(méi)有得到處理,很可能導(dǎo)致故障。因此我們需要把黑盒打開(kāi),對(duì)其中的每個(gè)小塊做白盒測(cè)試。
當(dāng)然,并不是所有的模塊測(cè)起來(lái)都這么輕松,前端用Node.js常干的事情就是寫(xiě)構(gòu)建插件和自動(dòng)化工具,典型的就是Gulp插件和命令行工具,那么這倆種特定的場(chǎng)景要怎么測(cè)試呢?
Gulp插件的測(cè)試現(xiàn)在前端構(gòu)建使用最多的就是Gulp了,它簡(jiǎn)明的API、流式構(gòu)建理念、以及在內(nèi)存中操作的性能,讓它備受追捧。雖然現(xiàn)在有像webpack這樣的后起之秀,但Gulp依舊憑借著其繁榮的生態(tài)圈擔(dān)當(dāng)著前端構(gòu)建的絕對(duì)主力。目前天貓前端就是使用Gulp作為代碼構(gòu)建工具。
用了Gulp作為構(gòu)建工具,也就免不了要開(kāi)發(fā)Gulp插件來(lái)滿足業(yè)務(wù)定制化的構(gòu)建需求,構(gòu)建過(guò)程本質(zhì)上其實(shí)是對(duì)源代碼進(jìn)行修改,如果修改過(guò)程中出現(xiàn)bug很可能直接導(dǎo)致線上故障。因此針對(duì)Gulp插件,尤其是會(huì)修改源代碼的Gulp插件一定要做仔細(xì)的測(cè)試來(lái)保證質(zhì)量。
又一個(gè)煎蛋的栗子比如這里有個(gè)煎蛋的Gulp插件,功能就是往所有js代碼前加一句注釋// 天貓前端招人,有意向的請(qǐng)發(fā)送簡(jiǎn)歷至lingyucoder@gmail.com,Gulp插件的代碼大概就是這樣:
"use strict"; const _ = require("lodash"); const through = require("through2"); const PluginError = require("gulp-util").PluginError; const DEFAULT_CONFIG = {}; module.exports = config => { config = _.defaults(config || {}, DEFAULT_CONFIG); return through.obj((file, encoding, callback) => { if (file.isStream()) return callback(new PluginError("gulp-welcome-to-tmall", `Stream is not supported`)); file.contents = new Buffer(`// 天貓前端招人,有意向的請(qǐng)發(fā)送簡(jiǎn)歷至lingyucoder@gmail.com ${file.contents.toString()}`); callback(null, file); }); };
對(duì)于這么一段代碼,怎么做測(cè)試呢?
一種方式就是直接偽造一個(gè)文件傳入,Gulp內(nèi)部實(shí)際上是通過(guò)vinyl-fs從操作系統(tǒng)讀取文件并做成虛擬文件對(duì)象,然后將這個(gè)虛擬文件對(duì)象交由through2創(chuàng)造的Transform來(lái)改寫(xiě)流中的內(nèi)容,而外層任務(wù)之間通過(guò)orchestrator控制,保證執(zhí)行順序(如果不了解可以看看這篇翻譯文章Gulp思維——Gulp高級(jí)技巧)。當(dāng)然一個(gè)插件不需要關(guān)心Gulp的任務(wù)管理機(jī)制,只需要關(guān)心傳入一個(gè)vinyl對(duì)象能否正確處理。因此只需要偽造一個(gè)虛擬文件對(duì)象傳給我們的Gulp插件就可以了。
首先設(shè)計(jì)測(cè)試用例,考慮兩個(gè)主要場(chǎng)景:
虛擬文件對(duì)象是流格式的,應(yīng)該拋出錯(cuò)誤
虛擬文件對(duì)象是Buffer格式的,能夠正常對(duì)文件內(nèi)容進(jìn)行加工,加工完的文件加上// 天貓前端招人,有意向的請(qǐng)發(fā)送簡(jiǎn)歷至lingyucoder@gmail.com的頭
對(duì)于第一個(gè)測(cè)試用例,我們需要?jiǎng)?chuàng)建一個(gè)流格式的vinyl對(duì)象。而對(duì)于各第二個(gè)測(cè)試用例,我們需要?jiǎng)?chuàng)建一個(gè)Buffer格式的vinyl對(duì)象。
當(dāng)然,首先我們需要一個(gè)被加工的源文件,放到test/src/testfile.js下吧:
"use strict"; console.log("hello world");
這個(gè)源文件非常簡(jiǎn)單,接下來(lái)的任務(wù)就是把它分別封裝成流格式的vinyl對(duì)象和Buffer格式的vinyl對(duì)象。
構(gòu)建一個(gè)Buffer格式的虛擬文件對(duì)象可以用vinyl-fs讀取操作系統(tǒng)里的文件生成vinyl對(duì)象,Gulp內(nèi)部也是使用它,默認(rèn)使用Buffer:
"use strict"; require("should"); const path = require("path"); const vfs = require("vinyl-fs"); const welcome = require("../index"); describe("welcome to Tmall", function() { it("should work when buffer", done => { vfs.src(path.join(__dirname, "src", "testfile.js")) .pipe(welcome()) .on("data", function(vf) { vf.contents.toString().should.be.eql(`// 天貓前端招人,有意向的請(qǐng)發(fā)送簡(jiǎn)歷至lingyucoder@gmail.com "use strict"; console.log("hello world"); `); done(); }); }); });
這樣測(cè)了Buffer格式后算是完成了主要功能的測(cè)試,那么要如何測(cè)試流格式呢?
方案一和上面一樣直接使用vinyl-fs,增加一個(gè)參數(shù)buffer: false即可:
把代碼修改成這樣:
"use strict"; require("should"); const path = require("path"); const vfs = require("vinyl-fs"); const PluginError = require("gulp-util").PluginError; const welcome = require("../index"); describe("welcome to Tmall", function() { it("should work when buffer", done => { // blabla }); it("should throw PluginError when stream", done => { vfs.src(path.join(__dirname, "src", "testfile.js"), { buffer: false }) .pipe(welcome()) .on("error", e => { e.should.be.instanceOf(PluginError); done(); }); }); });
這樣vinyl-fs直接從文件系統(tǒng)讀取文件并生成流格式的vinyl對(duì)象。
如果內(nèi)容并不來(lái)自于文件系統(tǒng),而是來(lái)源于一個(gè)已經(jīng)存在的可讀流,要怎么把它封裝成一個(gè)流格式的vinyl對(duì)象呢?
這樣的需求可以借助vinyl-source-stream:
"use strict"; require("should"); const fs = require("fs"); const path = require("path"); const source = require("vinyl-source-stream"); const vfs = require("vinyl-fs"); const PluginError = require("gulp-util").PluginError; const welcome = require("../index"); describe("welcome to Tmall", function() { it("should work when buffer", done => { // blabla }); it("should throw PluginError when stream", done => { fs.createReadStream(path.join(__dirname, "src", "testfile.js")) .pipe(source()) .pipe(welcome()) .on("error", e => { e.should.be.instanceOf(PluginError); done(); }); }); });
這里首先通過(guò)fs.createReadStream創(chuàng)建了一個(gè)可讀流,然后通過(guò)vinyl-source-stream把這個(gè)可讀流包裝成流格式的vinyl對(duì)象,并交給我們的插件做處理
模擬Gulp運(yùn)行Gulp插件執(zhí)行錯(cuò)誤時(shí)請(qǐng)拋出PluginError,這樣能夠讓gulp-plumber這樣的插件進(jìn)行錯(cuò)誤管理,防止錯(cuò)誤終止構(gòu)建進(jìn)程,這在gulp watch時(shí)非常有用
我們偽造的對(duì)象已經(jīng)可以跑通功能測(cè)試了,但是這數(shù)據(jù)來(lái)源終究是自己偽造的,并不是用戶日常的使用方式。如果采用最接近用戶使用的方式來(lái)做測(cè)試,測(cè)試結(jié)果才更加可靠和真實(shí)。那么問(wèn)題來(lái)了,怎么模擬真實(shí)的Gulp環(huán)境來(lái)做Gulp插件的測(cè)試呢?
首先模擬一下我們的項(xiàng)目結(jié)構(gòu):
test ├── build │?? └── testfile.js ├── gulpfile.js └── src └── testfile.js
一個(gè)簡(jiǎn)易的項(xiàng)目結(jié)構(gòu),源碼放在src下,通過(guò)gulpfile來(lái)指定任務(wù),構(gòu)建結(jié)果放在build下。按照我們平常使用方式在test目錄下搭好架子,并且寫(xiě)好gulpfile.js:
"use strict"; const gulp = require("gulp"); const welcome = require("../index"); const del = require("del"); gulp.task("clean", cb => del("build", cb)); gulp.task("default", ["clean"], () => { return gulp.src("src/**/*") .pipe(welcome()) .pipe(gulp.dest("build")); });
接著在測(cè)試代碼里來(lái)模擬Gulp運(yùn)行了,這里有兩種方案:
使用child_process庫(kù)提供的spawn或exec開(kāi)子進(jìn)程直接跑gulp命令,然后測(cè)試build目錄下是否是想要的結(jié)果
直接在當(dāng)前進(jìn)程獲取gulpfile中的Gulp實(shí)例來(lái)運(yùn)行Gulp任務(wù),然后測(cè)試build目錄下是否是想要的結(jié)果
開(kāi)子進(jìn)程進(jìn)行測(cè)試有一些坑,istanbul測(cè)試代碼覆蓋率時(shí)時(shí)無(wú)法跨進(jìn)程的,因此開(kāi)子進(jìn)程測(cè)試,首先需要子進(jìn)程執(zhí)行命令時(shí)加上istanbul,然后還需要手動(dòng)去收集覆蓋率數(shù)據(jù),當(dāng)開(kāi)啟多個(gè)子進(jìn)程時(shí)還需要自己做覆蓋率結(jié)果數(shù)據(jù)合并,相當(dāng)麻煩。
那么不開(kāi)子進(jìn)程怎么做呢?可以借助run-gulp-task這個(gè)工具來(lái)運(yùn)行,其內(nèi)部的機(jī)制就是首先獲取gulpfile文件內(nèi)容,在文件尾部加上module.exports = gulp;后require gulpfile從而獲取Gulp實(shí)例,然后將Gulp實(shí)例遞交給run-sequence調(diào)用內(nèi)部未開(kāi)放的APIgulp.run來(lái)運(yùn)行。
我們采用不開(kāi)子進(jìn)程的方式,把運(yùn)行Gulp的過(guò)程放在before鉤子中,測(cè)試代碼變成下面這樣:
"use strict"; require("should"); const path = require("path"); const run = require("run-gulp-task"); const CWD = process.cwd(); const fs = require("fs"); describe("welcome to Tmall", () => { before(done => { process.chdir(__dirname); run("default", path.join(__dirname, "gulpfile.js")) .catch(e => e) .then(e => { process.chdir(CWD); done(e); }); }); it("should work", function() { fs.readFileSync(path.join(__dirname, "build", "testfile.js")).toString().should.be.eql(`// 天貓前端招人,有意向的請(qǐng)發(fā)送簡(jiǎn)歷至lingyucoder@gmail.com "use strict"; console.log("hello world"); `); }); });
這樣由于不需要開(kāi)子進(jìn)程,代碼覆蓋率測(cè)試也可以和普通Node.js模塊一樣了
測(cè)試命令行輸出 雙一個(gè)煎蛋的栗子當(dāng)然前端寫(xiě)工具并不只限于Gulp插件,偶爾還會(huì)寫(xiě)一些輔助命令啥的,這些輔助命令直接在終端上運(yùn)行,結(jié)果也會(huì)直接展示在終端上。比如一個(gè)簡(jiǎn)單的使用commander實(shí)現(xiàn)的命令行工具:
// in index.js "use strict"; const program = require("commander"); const path = require("path"); const pkg = require(path.join(__dirname, "package.json")); program.version(pkg.version) .usage("[options]攔截輸出") .option("-t, --test", "Run test") .action((file, prog) => { if (prog.test) console.log("test"); }); module.exports = program; // in bin/cli #!/usr/bin/env node "use strict"; const program = require("../index.js"); program.parse(process.argv); !program.args[0] && program.help(); // in package.json { "bin": { "cli-test": "./bin/cli" } }
要測(cè)試命令行工具,自然要模擬用戶輸入命令,這一次依舊選擇不開(kāi)子進(jìn)程,直接用偽造一個(gè)process.argv交給program.parse即可。命令輸入了問(wèn)題也來(lái)了,數(shù)據(jù)是直接console.log的,要怎么攔截呢?
這可以借助sinon來(lái)攔截console.log,而且sinon非常貼心的提供了mocha-sinon方便測(cè)試用,這樣test.js大致就是這個(gè)樣子:
"use strict"; require("should"); require("mocha-sinon"); const program = require("../index"); const uncolor = require("uncolor"); describe("cli-test", () => { let rst; beforeEach(function() { this.sinon.stub(console, "log", function() { rst = arguments[0]; }); }); it("should print "test"", () => { program.parse([ "node", "./bin/cli", "-t", "file.js" ]); return uncolor(rst).trim().should.be.eql("test"); }); });
小結(jié)PS:由于命令行輸出時(shí)經(jīng)常會(huì)使用colors這樣的庫(kù)來(lái)添加顏色,因此在測(cè)試時(shí)記得用uncolor把這些顏色移除
Node.js相關(guān)的單元測(cè)試就扯這么多了,還有很多場(chǎng)景像服務(wù)器測(cè)試什么的就不扯了,因?yàn)槲也粫?huì)。當(dāng)然前端最主要的工作還是寫(xiě)頁(yè)面,接下來(lái)扯一扯如何對(duì)頁(yè)面上的組件做測(cè)試。
頁(yè)面測(cè)試對(duì)于瀏覽器里跑的前端代碼,做測(cè)試要比Node.js模塊要麻煩得多。Node.js模塊純js代碼,使用V8運(yùn)行在本地,測(cè)試用的各種各樣的依賴(lài)和工具都能快速的安裝,而前端代碼不僅僅要測(cè)試js,CSS等等,更麻煩的事需要模擬各種各樣的瀏覽器,比較常見(jiàn)的前端代碼測(cè)試方案有下面幾種:
構(gòu)建一個(gè)測(cè)試頁(yè)面,人肉直接到虛擬機(jī)上開(kāi)各種瀏覽器跑測(cè)試頁(yè)面(比如公司的f2etest)。這個(gè)方案的缺點(diǎn)就是不好做代碼覆蓋率測(cè)試,也不好持續(xù)化集成,同時(shí)人肉工作較多
使用PhantomJS構(gòu)建一個(gè)偽造的瀏覽器環(huán)境跑單元測(cè)試,好處是解決了代碼覆蓋率問(wèn)題,也可以做持續(xù)集成。這個(gè)方案的缺點(diǎn)是PhantomJS畢竟是Qt的webkit,并不是真實(shí)瀏覽器環(huán)境,PhantomJS也有各種各樣兼容性坑
通過(guò)Karma調(diào)用本機(jī)各種瀏覽器進(jìn)行測(cè)試,好處是可以跨瀏覽器做測(cè)試,也可以測(cè)試覆蓋率,但持續(xù)集成時(shí)需要注意只能開(kāi)PhantomJS做測(cè)試,畢竟集成的Linux環(huán)境不可能有瀏覽器。這可以說(shuō)是目前看到的最好的前端代碼測(cè)試方式了
叒一個(gè)煎蛋的栗子這里以gulp為構(gòu)建工具做測(cè)試,后面在React組件測(cè)試部分再介紹以webpack為構(gòu)建工具做測(cè)試
前端代碼依舊是js,一樣可以用Mocha+Should.js來(lái)做單元測(cè)試。打開(kāi)node_modules下的Mocha和Should.js,你會(huì)發(fā)現(xiàn)這些優(yōu)秀的開(kāi)源工具已經(jīng)非常貼心的提供了可在瀏覽器中直接運(yùn)行的版本:mocha/mocha.js和should/should.min.js,只需要把他們通過(guò)script標(biāo)簽引入即可,另外Mocha還需要引入自己的樣式mocha/mocha.css
首先看一下我們的前端項(xiàng)目結(jié)構(gòu):
. ├── gulpfile.js ├── package.json ├── src │?? └── index.js └── test ├── test.html └── test.js
比如這里源碼src/index.js就是定義一個(gè)全局函數(shù):
window.render = function() { var ctn = document.createElement("div"); ctn.setAttribute("id", "tmall"); ctn.appendChild(document.createTextNode("天貓前端招人,有意向的請(qǐng)發(fā)送簡(jiǎn)歷至lingyucoder@gmail.com")); document.body.appendChild(ctn); }
而測(cè)試頁(yè)面test/test.html大致上是這個(gè)樣子:
head里引入了測(cè)試框架Mocha和斷言庫(kù)Should.js,測(cè)試的結(jié)果會(huì)被顯示在這個(gè)容器里,而test/test.js里則是我們的測(cè)試的代碼。
前端頁(yè)面上測(cè)試和Node.js上測(cè)試沒(méi)啥太大不同,只是需要指定Mocha使用的UI,并需要手動(dòng)調(diào)用mocha.run():
mocha.ui("bdd"); describe("Welcome to Tmall", function() { before(function() { window.render(); }); it("Hello", function() { document.getElementById("tmall").textContent.should.be.eql("天貓前端招人,有意向的請(qǐng)發(fā)送簡(jiǎn)歷至lingyucoder@gmail.com"); }); }); mocha.run();
在瀏覽器里打開(kāi)test/test.html頁(yè)面,就可以看到效果了:
在不同的瀏覽器里打開(kāi)這個(gè)頁(yè)面,就可以看到當(dāng)前瀏覽器的測(cè)試了。這種方式能兼容最多的瀏覽器,當(dāng)然要跨機(jī)器之前記得把資源上傳到一個(gè)測(cè)試機(jī)器都能訪問(wèn)到的地方,比如CDN。
測(cè)試頁(yè)面有了,那么來(lái)試試接入PhantomJS吧
使用PhantomJS進(jìn)行測(cè)試PhantomJS是一個(gè)模擬的瀏覽器,它能執(zhí)行js,甚至還有webkit渲染引擎,只是沒(méi)有瀏覽器的界面上渲染結(jié)果罷了。我們可以使用它做很多事情,比如對(duì)網(wǎng)頁(yè)進(jìn)行截圖,寫(xiě)爬蟲(chóng)爬取異步渲染的頁(yè)面,以及接下來(lái)要介紹的——對(duì)頁(yè)面做測(cè)試。
當(dāng)然,這里我們不是直接使用PhantomJS,而是使用mocha-phantomjs來(lái)做測(cè)試。npm install --save-dev mocha-phantomjs安裝完成后,就可以運(yùn)行命令./node_modules/.bin/mocha-phantomjs ./test/test.html來(lái)對(duì)上面那個(gè)test/test.html的測(cè)試了:
單元測(cè)試沒(méi)問(wèn)題了,接下來(lái)就是代碼覆蓋率測(cè)試
覆蓋率打點(diǎn)首先第一步,改寫(xiě)我們的gulpfile.js:
"use strict"; const gulp = require("gulp"); const istanbul = require("gulp-istanbul"); gulp.task("test", function() { return gulp.src(["src/**/*.js"]) .pipe(istanbul({ coverageVariable: "__coverage__" })) .pipe(gulp.dest("build-test")); });
這里把覆蓋率結(jié)果保存到__coverage__里面,把打完點(diǎn)的代碼放到build-test目錄下,比如剛才的src/index.js的代碼,在運(yùn)行gulp test后,會(huì)生成build-test/index.js,內(nèi)容大致是這個(gè)樣子:
var __cov_WzFiasMcIh_mBvAjOuQiQg = (Function("return this"))(); if (!__cov_WzFiasMcIh_mBvAjOuQiQg.__coverage__) { __cov_WzFiasMcIh_mBvAjOuQiQg.__coverage__ = {}; } __cov_WzFiasMcIh_mBvAjOuQiQg = __cov_WzFiasMcIh_mBvAjOuQiQg.__coverage__; if (!(__cov_WzFiasMcIh_mBvAjOuQiQg["/Users/lingyu/gitlab/dev/mui/test-page/src/index.js"])) { __cov_WzFiasMcIh_mBvAjOuQiQg["/Users/lingyu/gitlab/dev/mui/test-page/src/index.js"] = {"path":"/Users/lingyu/gitlab/dev/mui/test-page/src/index.js","s":{"1":0,"2":0,"3":0,"4":0,"5":0},"b":{},"f":{"1":0},"fnMap":{"1":{"name":"(anonymous_1)","line":1,"loc":{"start":{"line":1,"column":16},"end":{"line":1,"column":27}}}},"statementMap":{"1":{"start":{"line":1,"column":0},"end":{"line":6,"column":1}},"2":{"start":{"line":2,"column":2},"end":{"line":2,"column":42}},"3":{"start":{"line":3,"column":2},"end":{"line":3,"column":34}},"4":{"start":{"line":4,"column":2},"end":{"line":4,"column":85}},"5":{"start":{"line":5,"column":2},"end":{"line":5,"column":33}}},"branchMap":{}}; } __cov_WzFiasMcIh_mBvAjOuQiQg = __cov_WzFiasMcIh_mBvAjOuQiQg["/Users/lingyu/gitlab/dev/mui/test-page/src/index.js"]; __cov_WzFiasMcIh_mBvAjOuQiQg.s["1"]++;window.render=function(){__cov_WzFiasMcIh_mBvAjOuQiQg.f["1"]++;__cov_WzFiasMcIh_mBvAjOuQiQg.s["2"]++;var ctn=document.createElement("div");__cov_WzFiasMcIh_mBvAjOuQiQg.s["3"]++;ctn.setAttribute("id","tmall");__cov_WzFiasMcIh_mBvAjOuQiQg.s["4"]++;ctn.appendChild(document.createTextNode("天貓前端招人uFF0C有意向的請(qǐng)發(fā)送簡(jiǎn)歷至lingyucoder@gmail.com"));__cov_WzFiasMcIh_mBvAjOuQiQg.s["5"]++;document.body.appendChild(ctn);};
這都什么鬼!不管了,反正運(yùn)行它就好。把test/test.html里面引入的代碼從src/index.js修改為build-test/index.js,保證頁(yè)面運(yùn)行時(shí)使用的是編譯后的代碼。
編寫(xiě)鉤子運(yùn)行數(shù)據(jù)會(huì)存放到變量__coverage__里,但是我們還需要一段鉤子代碼在單元測(cè)試結(jié)束后獲取這個(gè)變量里的內(nèi)容。把鉤子代碼放在test/hook.js下,里面內(nèi)容這樣寫(xiě):
"use strict"; var fs = require("fs"); module.exports = { afterEnd: function(runner) { var coverage = runner.page.evaluate(function() { return window.__coverage__; }); if (coverage) { console.log("Writing coverage to coverage/coverage.json"); fs.write("coverage/coverage.json", JSON.stringify(coverage), "w"); } else { console.log("No coverage data generated"); } } };
這樣準(zhǔn)備工作工作就大功告成了,執(zhí)行命令./node_modules/.bin/mocha-phantomjs ./test/test.html --hooks ./test/hook.js,可以看到如下圖結(jié)果,同時(shí)覆蓋率結(jié)果被寫(xiě)入到coverage/coverage.json里面了。
生成頁(yè)面有了結(jié)果覆蓋率結(jié)果就可以生成覆蓋率頁(yè)面了,首先看看覆蓋率概況吧。執(zhí)行命令./node_modules/.bin/istanbul report --root coverage text-summary,可以看到下圖:
還是原來(lái)的配方,還是想熟悉的味道。接下來(lái)運(yùn)行./node_modules/.bin/istanbul report --root coverage lcov生成覆蓋率頁(yè)面,執(zhí)行完后open coverage/lcov-report/index.html,點(diǎn)擊進(jìn)入到src/index.js:
一顆賽艇!這樣我們對(duì)前端代碼就能做覆蓋率測(cè)試了
接入KarmaKarma是一個(gè)測(cè)試集成框架,可以方便地以插件的形式集成測(cè)試框架、測(cè)試環(huán)境、覆蓋率工具等等。Karma已經(jīng)有了一套相當(dāng)完善的插件體系,這里嘗試在PhantomJS、Chrome、FireFox下做測(cè)試,首先需要使用npm安裝一些依賴(lài):
karma:框架本體
karma-mocha:Mocha測(cè)試框架
karma-coverage:覆蓋率測(cè)試
karma-spec-reporter:測(cè)試結(jié)果輸出
karma-phantomjs-launcher:PhantomJS環(huán)境
phantomjs-prebuilt: PhantomJS最新版本
karma-chrome-launcher:Chrome環(huán)境
karma-firefox-launcher:Firefox環(huán)境
安裝完成后,就可以開(kāi)啟我們的Karma之旅了。還是之前的那個(gè)項(xiàng)目,我們把該清除的清除,只留下源文件和而是文件,并增加一個(gè)karma.conf.js文件:
. ├── karma.conf.js ├── package.json ├── src │?? └── index.js └── test └── test.js
karma.conf.js是Karma框架的配置文件,在這個(gè)例子里,它大概是這個(gè)樣子:
"use strict"; module.exports = function(config) { config.set({ frameworks: ["mocha"], files: [ "./node_modules/should/should.js", "src/**/*.js", "test/**/*.js" ], preprocessors: { "src/**/*.js": ["coverage"] }, plugins: ["karma-mocha", "karma-phantomjs-launcher", "karma-chrome-launcher", "karma-firefox-launcher", "karma-coverage", "karma-spec-reporter"], browsers: ["PhantomJS", "Firefox", "Chrome"], reporters: ["spec", "coverage"], coverageReporter: { dir: "coverage", reporters: [{ type: "json", subdir: ".", file: "coverage.json", }, { type: "lcov", subdir: "." }, { type: "text-summary" }] } }); };
這些配置都是什么意思呢?這里挨個(gè)說(shuō)明一下:
frameworks: 使用的測(cè)試框架,這里依舊是我們熟悉又親切的Mocha
files:測(cè)試頁(yè)面需要加載的資源,上面的test目錄下已經(jīng)沒(méi)有test.html了,所有需要加載內(nèi)容都在這里指定,如果是CDN上的資源,直接寫(xiě)URL也可以,不過(guò)建議盡可能使用本地資源,這樣測(cè)試更快而且即使沒(méi)網(wǎng)也可以測(cè)試。這個(gè)例子里,第一行載入的是斷言庫(kù)Should.js,第二行是src下的所有代碼,第三行載入測(cè)試代碼
preprocessors:配置預(yù)處理器,在上面files載入對(duì)應(yīng)的文件前,如果在這里配置了預(yù)處理器,會(huì)先對(duì)文件做處理,然后載入處理結(jié)果。這個(gè)例子里,需要對(duì)src目錄下的所有資源添加覆蓋率打點(diǎn)(這一步之前是通過(guò)gulp-istanbul來(lái)做,現(xiàn)在karma-coverage框架可以很方便的處理,也不需要鉤子啥的了)。后面做React組件測(cè)試時(shí)也會(huì)在這里使用webpack
plugins:安裝的插件列表
browsers:需要測(cè)試的瀏覽器,這里我們選擇了PhantomJS、FireFox、Chrome
reporters:需要生成哪些代碼報(bào)告
coverageReporter:覆蓋率報(bào)告要如何生成,這里我們期望生成和之前一樣的報(bào)告,包括覆蓋率頁(yè)面、lcov.info、coverage.json、以及命令行里的提示
好了,配置完成,來(lái)試試吧,運(yùn)行./node_modules/karma/bin/karma start --single-run,可以看到如下輸出:
可以看到,Karma首先會(huì)在9876端口開(kāi)啟一個(gè)本地服務(wù),然后分別啟動(dòng)PhantomJS、FireFox、Chrome去加載這個(gè)頁(yè)面,收集到測(cè)試結(jié)果信息之后分別輸出,這樣跨瀏覽器測(cè)試就解決啦。如果要新增瀏覽器就安裝對(duì)應(yīng)的瀏覽器插件,然后在browsers里指定一下即可,非常靈活方便。
那如果我的mac電腦上沒(méi)有IE,又想測(cè)IE,怎么辦呢?可以直接運(yùn)行./node_modules/karma/bin/karma start啟動(dòng)本地服務(wù)器,然后使用其他機(jī)器開(kāi)對(duì)應(yīng)瀏覽器直接訪問(wèn)本機(jī)的9876端口(當(dāng)然這個(gè)端口是可配置的)即可,同樣移動(dòng)端的測(cè)試也可以采用這個(gè)方法。這個(gè)方案兼顧了前兩個(gè)方案的優(yōu)點(diǎn),彌補(bǔ)了其不足,是目前看到最優(yōu)秀的前端代碼測(cè)試方案了
React組件測(cè)試去年React旋風(fēng)一般席卷全球,當(dāng)然天貓也在技術(shù)上緊跟時(shí)代腳步。天貓商家端業(yè)務(wù)已經(jīng)全面切入React,形成了React組件體系,幾乎所有新業(yè)務(wù)都采用React開(kāi)發(fā),而老業(yè)務(wù)也在不斷向React遷移。React大紅大紫,這里多帶帶拉出來(lái)講一講React+webpack的打包方案如何進(jìn)行測(cè)試
叕一個(gè)煎蛋的栗子這里只聊React Web,不聊React Native
事實(shí)上天貓目前并未采用webpack打包,而是Gulp+Babel編譯React CommonJS代碼成AMD模塊使用,這是為了能夠在新老業(yè)務(wù)使用上更加靈活,當(dāng)然也有部分業(yè)務(wù)采用webpack打包并上線
這里創(chuàng)建一個(gè)React組件,目錄結(jié)構(gòu)大致這樣(這里略過(guò)CSS相關(guān)部分,只要跑通了,集成CSS像PostCSS、Less都沒(méi)啥問(wèn)題):
. ├── demo ├── karma.conf.js ├── package.json ├── src │?? └── index.jsx ├── test │?? └── index_spec.jsx ├── webpack.dev.js └── webpack.pub.js
React組件源碼src/index.jsx大概是這個(gè)樣子:
import React from "react"; class Welcome extends React.Component { constructor() { super(); } render() { return{this.props.content}; } } Welcome.displayName = "Welcome"; Welcome.propTypes = { /** * content of element */ content: React.PropTypes.string }; Welcome.defaultProps = { content: "Hello Tmall" }; module.exports = Welcome;
那么對(duì)應(yīng)的test/index_spec.jsx則大概是這個(gè)樣子:
import "should"; import Welcome from "../src/index.jsx"; import ReactDOM from "react-dom"; import React from "react"; import TestUtils from "react-addons-test-utils"; describe("test", function() { const container = document.createElement("div"); document.body.appendChild(container); afterEach(() => { ReactDOM.unmountComponentAtNode(container); }); it("Hello Tmall", function() { let cp = ReactDOM.render(, container); let welcome = TestUtils.findRenderedComponentWithType(cp, Welcome); ReactDOM.findDOMnode(welcome).textContent.should.be.eql("Hello Tmall"); }); });
由于是測(cè)試React,自然要使用React的TestUtils,這個(gè)工具庫(kù)提供了不少方便查找節(jié)點(diǎn)和組件的方法,最重要的是它提供了模擬事件的API,這可以說(shuō)是UI測(cè)試最重要的一個(gè)功能。更多關(guān)于TestUtils的使用請(qǐng)參考React官網(wǎng),這里就不扯了...
代碼有了,測(cè)試用例也有了,接下就差跑起來(lái)了。karma.conf.js肯定就和上面不一樣了,首先它要多一個(gè)插件karma-webpack,因?yàn)槲覀兊腞eact組件是需要webpack打包的,不打包的代碼壓根就沒(méi)法運(yùn)行。另外還需要注意代碼覆蓋率測(cè)試也出現(xiàn)了變化。因?yàn)楝F(xiàn)在多了一層Babel編譯,Babel編譯ES6、ES7源碼生成ES5代碼后會(huì)產(chǎn)生很多polyfill代碼,因此如果對(duì)build完成之后的代碼做覆蓋率測(cè)試會(huì)包含這些polyfill代碼,這樣測(cè)出來(lái)的覆蓋率顯然是不可靠的,這個(gè)問(wèn)題可以通過(guò)isparta-loader來(lái)解決。React組件的karma.conf.js大概是這個(gè)樣子:
"use strict"; const path = require("path"); module.exports = function(config) { config.set({ frameworks: ["mocha"], files: [ "./node_modules/phantomjs-polyfill/bind-polyfill.js", "test/**/*_spec.jsx" ], plugins: ["karma-webpack", "karma-mocha",, "karma-chrome-launcher", "karma-firefox-launcher", "karma-phantomjs-launcher", "karma-coverage", "karma-spec-reporter"], browsers: ["PhantomJS", "Firefox", "Chrome"], preprocessors: { "test/**/*_spec.jsx": ["webpack"] }, reporters: ["spec", "coverage"], coverageReporter: { dir: "coverage", reporters: [{ type: "json", subdir: ".", file: "coverage.json", }, { type: "lcov", subdir: "." }, { type: "text-summary" }] }, webpack: { module: { loaders: [{ test: /.jsx?/, loaders: ["babel"] }], preLoaders: [{ test: /.jsx?$/, include: [path.resolve("src/")], loader: "isparta" }] } }, webpackMiddleware: { noInfo: true } }); };
這里相對(duì)于之前的karma.conf.js,主要有以下幾點(diǎn)區(qū)別:
由于webpack的打包功能,我們?cè)跍y(cè)試代碼里直接import組件代碼,因此不再需要在files里手動(dòng)引入組件代碼
預(yù)處理里面需要對(duì)每個(gè)測(cè)試文件都做webpack打包
添加webpack編譯相關(guān)配置,在編譯源碼時(shí),需要定義preLoaders,并使用isparta-loader做代碼覆蓋率打點(diǎn)
添加webpackMiddleware配置,這里noInfo作用是不需要輸出webpack編譯時(shí)那一大串信息
這樣配置基本上就完成了,跑一把./node_modules/karma/bin/karma start --single-run:
很好,結(jié)果符合預(yù)期。open coverage/lcov-report/index.html打開(kāi)覆蓋率頁(yè)面:
鵝妹子音!??!直接對(duì)jsx代碼做的覆蓋率測(cè)試!這樣React組件的測(cè)試大體上就完工了
小結(jié)前端的代碼測(cè)試主要難度是如何模擬各種各樣的瀏覽器環(huán)境,Karma給我們提供了很好地方式,對(duì)于本地有的瀏覽器能自動(dòng)打開(kāi)并測(cè)試,本地沒(méi)有的瀏覽器則提供直接訪問(wèn)的頁(yè)面。前端尤其是移動(dòng)端瀏覽器種類(lèi)繁多,很難做到完美,但我們可以通過(guò)這種方式實(shí)現(xiàn)主流瀏覽器的覆蓋,保證每次上線大多數(shù)用戶沒(méi)有問(wèn)題。
持續(xù)集成測(cè)試結(jié)果有了,接下來(lái)就是把這些測(cè)試結(jié)果接入到持續(xù)集成之中。持續(xù)集成是一種非常優(yōu)秀的多人開(kāi)發(fā)實(shí)踐,通過(guò)代碼push觸發(fā)鉤子,實(shí)現(xiàn)自動(dòng)運(yùn)行編譯、測(cè)試等工作。接入持續(xù)集成后,我們的每一次push代碼,每個(gè)Merge Request都會(huì)生成對(duì)應(yīng)的測(cè)試結(jié)果,項(xiàng)目的其他成員可以很清楚地了解到新代碼是否影響了現(xiàn)有的功能,在接入自動(dòng)告警后,可以在代碼提交階段就快速發(fā)現(xiàn)錯(cuò)誤,提升開(kāi)發(fā)迭代效率。
持續(xù)集成會(huì)在每次集成時(shí)提供一個(gè)幾乎空白的虛擬機(jī)器,并拷貝用戶提交的代碼到機(jī)器本地,通過(guò)讀取用戶項(xiàng)目下的持續(xù)集成配置,自動(dòng)化的安裝環(huán)境和依賴(lài),編譯和測(cè)試完成后生成報(bào)告,在一段時(shí)間之后釋放虛擬機(jī)器資源。
開(kāi)源的持續(xù)集成開(kāi)源比較出名的持續(xù)集成服務(wù)當(dāng)屬Travis,而代碼覆蓋率則通過(guò)Coveralls,只要有GitHub賬戶,就可以很輕松的接入Travis和Coveralls,在網(wǎng)站上勾選了需要持續(xù)集成的項(xiàng)目以后,每次代碼push就會(huì)觸發(fā)自動(dòng)化測(cè)試。這兩個(gè)網(wǎng)站在跑完測(cè)試以后,會(huì)自動(dòng)生成測(cè)試結(jié)果的小圖片
Travis會(huì)讀取項(xiàng)目下的travis.yml文件,一個(gè)簡(jiǎn)單的例子:
language: node_js node_js: - "stable" - "4.0.0" - "5.0.0" script: "npm run test" after_script: "npm install coveralls@2.10.0 && cat ./coverage/lcov.info | coveralls"
language定義了運(yùn)行環(huán)境的語(yǔ)言,而對(duì)應(yīng)的node_js可以定義需要在哪幾個(gè)Node.js版本做測(cè)試,比如這里的定義,代表著會(huì)分別在最新穩(wěn)定版、4.0.0、5.0.0版本的Node.js環(huán)境下做測(cè)試
而script則是測(cè)試?yán)玫拿?,一般情況下,都應(yīng)該把自己這個(gè)項(xiàng)目開(kāi)發(fā)所需要的命令都寫(xiě)在package.json的scripts里面,比如我們的測(cè)試方法./node_modules/karma/bin/karma start --single-run就應(yīng)當(dāng)這樣寫(xiě)到scripts里:
{ "scripts": { "test": "./node_modules/karma/bin/karma start --single-run" } }
而after_script則是在測(cè)試完成之后運(yùn)行的命令,這里需要上傳覆蓋率結(jié)果到coveralls,只需要安裝coveralls庫(kù),然后獲取lcov.info上傳給Coveralls即可
更多配置請(qǐng)參照Travis官網(wǎng)介紹
這樣配置后,每次push的結(jié)果都可以上Travis和Coveralls看構(gòu)建和代碼覆蓋率結(jié)果了
小結(jié)項(xiàng)目接入持續(xù)集成在多人開(kāi)發(fā)同一個(gè)倉(cāng)庫(kù)時(shí)候能起到很大的用途,每次push都能自動(dòng)觸發(fā)測(cè)試,測(cè)試沒(méi)過(guò)會(huì)發(fā)生告警。如果需求采用Issues+Merge Request來(lái)管理,每個(gè)需求一個(gè)Issue+一個(gè)分支,開(kāi)發(fā)完成后提交Merge Request,由項(xiàng)目Owner負(fù)責(zé)合并,項(xiàng)目質(zhì)量將更有保障
總結(jié)這里只是前端測(cè)試相關(guān)知識(shí)的一小部分,還有非常多的內(nèi)容可以深入挖掘,而測(cè)試也僅僅是前端流程自動(dòng)化的一部分。在前端技術(shù)快速發(fā)展的今天,前端項(xiàng)目不再像當(dāng)年的刀耕火種一般,越來(lái)越多的軟件工程經(jīng)驗(yàn)被集成到前端項(xiàng)目中,前端項(xiàng)目正向工程化、流程化、自動(dòng)化方向高速奔跑。還有更多優(yōu)秀的提升開(kāi)發(fā)效率、保證開(kāi)發(fā)質(zhì)量的自動(dòng)化方案亟待我們挖掘。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/86273.html
摘要:歡迎大家收看聊一聊系列,這一套系列文章,可以幫助前端工程師們了解前端的方方面面不僅僅是代碼從說(shuō)起要想了解,得從一個(gè)新的規(guī)則說(shuō)起。因?yàn)橛脩魶](méi)有安裝的話,我們強(qiáng)制要求顯示也沒(méi)有辦法。國(guó)內(nèi)有阿里巴巴的平臺(tái),可以選自己喜歡的圖標(biāo)導(dǎo)出。 歡迎大家收看聊一聊系列,這一套系列文章,可以幫助前端工程師們了解前端的方方面面(不僅僅是代碼):https://segmentfault.com/blog/fr...
摘要:歡迎大家收看聊一聊系列,這一套系列文章,可以幫助前端工程師們了解前端的方方面面不僅僅是代碼從說(shuō)起要想了解,得從一個(gè)新的規(guī)則說(shuō)起。因?yàn)橛脩魶](méi)有安裝的話,我們強(qiáng)制要求顯示也沒(méi)有辦法。國(guó)內(nèi)有阿里巴巴的平臺(tái),可以選自己喜歡的圖標(biāo)導(dǎo)出。 歡迎大家收看聊一聊系列,這一套系列文章,可以幫助前端工程師們了解前端的方方面面(不僅僅是代碼):https://segmentfault.com/blog/fr...
摘要:歡迎大家收看聊一聊系列,這一套系列文章,可以幫助前端工程師們了解前端的方方面面不僅僅是代碼從說(shuō)起要想了解,得從一個(gè)新的規(guī)則說(shuō)起。因?yàn)橛脩魶](méi)有安裝的話,我們強(qiáng)制要求顯示也沒(méi)有辦法。國(guó)內(nèi)有阿里巴巴的平臺(tái),可以選自己喜歡的圖標(biāo)導(dǎo)出。 歡迎大家收看聊一聊系列,這一套系列文章,可以幫助前端工程師們了解前端的方方面面(不僅僅是代碼):https://segmentfault.com/blog/fr...
摘要:性能統(tǒng)計(jì)有助于幫我們檢測(cè)網(wǎng)站的用戶體驗(yàn)。這樣,我們就輕輕松松的統(tǒng)計(jì)到了首屏?xí)r間。下一章,我們將繼續(xù)聊聊百度移動(dòng)版首頁(yè)那些事。 歡迎大家收看聊一聊系列,這一套系列文章,可以幫助前端工程師們了解前端的方方面面(不僅僅是代碼): https://segmentfault.com/blog/frontenddriver 上一篇文章我們討論了,如何進(jìn)行前端日志打點(diǎn)統(tǒng)計(jì): https://segm...
閱讀 2497·2021-10-14 09:43
閱讀 2544·2021-09-09 09:34
閱讀 1666·2019-08-30 12:57
閱讀 1267·2019-08-29 14:16
閱讀 791·2019-08-26 12:13
閱讀 3261·2019-08-26 11:45
閱讀 2357·2019-08-23 16:18
閱讀 2735·2019-08-23 15:27