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