成人无码视频,亚洲精品久久久久av无码,午夜精品久久久久久毛片,亚洲 中文字幕 日韩 无码

資訊專(zhuān)欄INFORMATION COLUMN

聊一聊前端自動(dòng)化測(cè)試

wthee / 3091人閱讀

摘要:在真正寫(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è)試參與了。

本文轉(zhuǎn)載自 天貓前端博客,更多精彩文章請(qǐng)進(jìn)入天貓前端博客查看

前言 為何要測(cè)試

以前不喜歡寫(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è)試用例為粒度,可以使用beforeEachafterEach,這兩個(gè)鉤子則會(huì)分別在該分組每個(gè)測(cè)試用例運(yùn)行前和運(yùn)行后執(zhí)行。由于很多代碼都需要模擬環(huán)境,可以再這些beforebeforeEach做這些準(zhǔn)備工作,然后在afterafterEach里做回收操作。

異步代碼的測(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ì)象。

注意:使用should測(cè)試Promise對(duì)象時(shí),請(qǐng)一定要return,一定要return,一定要return,否則斷言將無(wú)效

異步運(yùn)行測(cè)試

有時(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.jsonscripts字段中,比如:

{
  "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)建Buffer格式的虛擬文件對(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è)試流格式呢?

構(gòu)建流格式的虛擬文件對(duì)象

方案一和上面一樣直接使用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插件執(zhí)行錯(cuò)誤時(shí)請(qǐng)拋出PluginError,這樣能夠讓gulp-plumber這樣的插件進(jìn)行錯(cuò)誤管理,防止錯(cuò)誤終止構(gòu)建進(jìn)程,這在gulp watch時(shí)非常有用

模擬Gulp運(yùn)行

我們偽造的對(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ù)提供的spawnexec開(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");
  });
});

PS:由于命令行輸出時(shí)經(jīng)常會(huì)使用colors這樣的庫(kù)來(lái)添加顏色,因此在測(cè)試時(shí)記得用uncolor把這些顏色移除

小結(jié)

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è)試方式了

這里以gulp為構(gòu)建工具做測(cè)試,后面在React組件測(cè)試部分再介紹以webpack為構(gòu)建工具做測(cè)試

叒一個(gè)煎蛋的栗子

前端代碼依舊是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.jsshould/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è)試了

接入Karma

Karma是一個(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è)試

這里只聊React Web,不聊React Native

事實(shí)上天貓目前并未采用webpack打包,而是Gulp+Babel編譯React CommonJS代碼成AMD模塊使用,這是為了能夠在新老業(yè)務(wù)使用上更加靈活,當(dāng)然也有部分業(yè)務(wù)采用webpack打包并上線

叕一個(gè)煎蛋的栗子

這里創(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

相關(guān)文章

  • [一聊系列]一聊iconfont那些事兒

    摘要:歡迎大家收看聊一聊系列,這一套系列文章,可以幫助前端工程師們了解前端的方方面面不僅僅是代碼從說(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...

    Markxu 評(píng)論0 收藏0
  • [一聊系列]一聊iconfont那些事兒

    摘要:歡迎大家收看聊一聊系列,這一套系列文章,可以幫助前端工程師們了解前端的方方面面不僅僅是代碼從說(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...

    2501207950 評(píng)論0 收藏0
  • [一聊系列]一聊iconfont那些事兒

    摘要:歡迎大家收看聊一聊系列,這一套系列文章,可以幫助前端工程師們了解前端的方方面面不僅僅是代碼從說(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...

    Shisui 評(píng)論0 收藏0
  • [一聊系列]一聊前端速度統(tǒng)計(jì)(性能統(tǒng)計(jì))那些事兒

    摘要:性能統(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...

    gclove 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

閱讀需要支付1元查看
<