新聞中心
代碼部署之前,進(jìn)行一定的單元測(cè)試是十分必要的,這樣能夠有效并且持續(xù)保證代碼質(zhì)量。而實(shí)踐表明,高質(zhì)量的單元測(cè)試還可以幫助我們完善自己的代碼。這篇博客將通過(guò)一些簡(jiǎn)單的測(cè)試案例,介紹幾款Node.js測(cè)試模塊: Mocha和Should,SuperTest。本文側(cè)重于解釋原理,各個(gè)模塊的詳細(xì)使用案例以后單獨(dú)再聊。

為啥需要單元測(cè)試?
所謂單元測(cè)試,就是對(duì)某個(gè)函數(shù)或者API進(jìn)行正確性驗(yàn)證。來(lái)看個(gè)簡(jiǎn)單的例子add1.js:
function add(a, b) {
return a + b;
}
沒(méi)錯(cuò),我寫(xiě)了一個(gè)加法函數(shù)。這有啥好測(cè)的呢?不妨用node執(zhí)行一下:
> add = function(a, b){return a + b}
[Function: add] > add(4) NaN
當(dāng)add函數(shù)僅給定一個(gè)參數(shù)4的時(shí)候,a為4,b為undefined,兩者相加為NaN。
- 你考慮過(guò)只有一個(gè)參數(shù)的場(chǎng)景嗎?
- 給定一個(gè)參數(shù)時(shí),NaN是你想要的結(jié)果嗎?
- 如果參數(shù)不是整數(shù)怎么辦?
這時(shí),就需要單元測(cè)試來(lái)驗(yàn)證各種可能的場(chǎng)景了。
如果我把a(bǔ)dd函數(shù)定義為兩個(gè)整數(shù)相加,而其他輸入則返回undefined,那么正確的代碼add2.js應(yīng)該是這樣的:
function add(a, b) {
if (typeof a === "number" && typeof b === "number")
{
return a + b;
}
else
{
return undefined;
}
}
發(fā)現(xiàn)一個(gè)有趣的現(xiàn)象,我們寫(xiě)代碼的時(shí)候很容易陷入思維漏洞,而寫(xiě)測(cè)試的時(shí)候往往會(huì)考慮各種情況,這就是所謂的TDD(Test-Driven-Development: 測(cè)試驅(qū)動(dòng)開(kāi)發(fā))的神奇之處。因此,進(jìn)行一定的單元測(cè)試是十分必要的:
- 驗(yàn)證代碼的正確性
- 避免修改代碼時(shí)出錯(cuò)
- 避免其他團(tuán)隊(duì)成員修改代碼時(shí)出錯(cuò)
- 便于自動(dòng)化測(cè)試與部署
測(cè)試框架 - Mocha
下面的測(cè)試代碼test2.js用于測(cè)試add2.js。這里使用了測(cè)試框架Mocha以及Node.js自帶的斷言庫(kù)Assert。
var add = require("../add2.js");
var assert = require("assert");
// 當(dāng)2個(gè)參數(shù)均為整數(shù)時(shí)
it("should return 3", function() {
var sum = add(1, 2);
assert.equal(sum, 3);
});
// 當(dāng)?shù)?個(gè)參數(shù)為String時(shí)
it("should return undefined", function() {
var sum = add(1, "2");
assert.equal(sum, undefined);
});
// 當(dāng)只有1個(gè)參數(shù)時(shí)
it("should return undefined", function() {
var sum = add(1);
assert.equal(sum, undefined);
});
測(cè)試代碼中使用了測(cè)試框架Mocha提供的it函數(shù),3個(gè)it函數(shù)分別測(cè)試了3種不同的案例(test case)。it函數(shù)的第1個(gè)參數(shù)為字符串,用于描述測(cè)試,一般會(huì)寫(xiě)期望得到的結(jié)果,例如”should return 3”; 而第2個(gè)參數(shù)為函數(shù),用于編寫(xiě)測(cè)試代碼,一般是先調(diào)用被測(cè)試的函數(shù)或者API,獲取結(jié)果之后,使用斷言庫(kù)判斷執(zhí)行結(jié)果是否正確。
測(cè)試代碼中使用了Node.js自帶的斷言庫(kù)Assert的assert.equal函數(shù),用于判定add函數(shù)返回的結(jié)果是否正確。assert.equal成功時(shí)不會(huì)發(fā)生什么,而失敗時(shí)會(huì)拋出一個(gè)AssertionError。不妨使用node測(cè)試一下:
> assert = require("assert");
> assert.equal(1, 1);
undefined
> assert.equal(1, 2);
AssertionError: 1 == 2
at repl:1:8
at sigintHandlersWrap (vm.js:22:35)
at sigintHandlersWrap (vm.js:96:12)
at ContextifyScript.Script.runInThisContext (vm.js:21:12)
at REPLServer.defaultEval (repl.js:313:29)
at bound (domain.js:280:14)
at REPLServer.runBound [as eval] (domain.js:293:12)
at REPLServer. (repl.js:513:10)
at emitOne (events.js:101:20)
at REPLServer.emit (events.js:188:7)
原理:
我們按照Mocha的it函數(shù)編寫(xiě)一個(gè)個(gè)測(cè)試案例,然后Mocha負(fù)責(zé)執(zhí)行這些案例;當(dāng)assert.equal斷言成功時(shí),則測(cè)試案例通過(guò);當(dāng)assert.equal斷言失敗時(shí),拋出AssertionError,Mocha能夠捕獲到這些異常,然后對(duì)應(yīng)的測(cè)試案例失敗。
使用mocha執(zhí)行test2.js:
mocha test/test2.js
下面為輸出,表示測(cè)試案例全部通過(guò)
should return 3 should return undefined should return undefined 3 passing
而當(dāng)我們使用test1.js測(cè)試add1.js時(shí),則后面2個(gè)測(cè)試案例失敗:
should return 3
1) should return undefined
2) should return undefined
1 passing (14ms)
2 failing
1) should return undefined:
AssertionError: '12' == undefined
at Context. (test/test1.js:18:12)
2) should return undefined:
AssertionError: NaN == undefined
at Context. (test/test1.js:25:12)
斷言庫(kù) - Should
Node.js自帶的斷言庫(kù)Assert提供的函數(shù)有限,在實(shí)際工作中,Should等第三方斷言庫(kù)則更加強(qiáng)大和實(shí)用。
我寫(xiě)了一個(gè)merge函數(shù)merge.js,實(shí)現(xiàn)了類(lèi)似于_.extend()與Object.assign()的功能,用于合并兩個(gè)Object的屬性。
function merge(a, b) {
if (typeof a === "object" && typeof b === "object")
{
for (var property in b)
{
a[property] = b[property];
}
return a;
}
else
{
return undefined;
}
}
然后我使用Should寫(xiě)了對(duì)應(yīng)的測(cè)試代碼test3.js:
require("should");
var merge = require("../merge.js");
// 當(dāng)2個(gè)參數(shù)均為對(duì)象時(shí)
it("should success", function() {
var a = {
name: "Fundebug",
type: "SaaS"
};
var b = {
service: "Real time bug monitoring",
product:
{
frontend: "JavaScript",
backend: "Node.js",
mobile: "微信小程序"
}
};
var c = merge(a, b);
c.should.have.property("name", "Fundebug");
c.should.have.propertyByPath("product", "frontend").equal("JavaScript");
});
// 當(dāng)只有1個(gè)參數(shù)時(shí)
it("should return undefined", function() {
var a = {
name: "Fundebug",
type: "SaaS"
};
var c = merge(a);
(typeof c).should.equal("undefined");
});
測(cè)試代碼稍微有點(diǎn)長(zhǎng),但是使用Should的只有三處:
c.should.have.property("name", "Fundebug");
c.should.have.propertyByPath("product", "frontend").equal("JavaScript");
(typeof c).should.equal("undefined");
可知Should能夠:
- 驗(yàn)證對(duì)象是否存在某屬性,并驗(yàn)證其取值
- 驗(yàn)證對(duì)象是否存在某個(gè)嵌套屬性,并使用鏈?zhǔn)椒绞津?yàn)證其取值
那么Should為什么不能直接驗(yàn)證c的取值為undefined呢?比如這樣寫(xiě):
c.should.equal(undefined); // 這樣寫(xiě)是錯(cuò)誤的
原理:
Should會(huì)為每個(gè)對(duì)象添加should屬性,然后通過(guò)該屬性提供各種斷言函數(shù),我們可以使用這些函數(shù)驗(yàn)證對(duì)象的取值。對(duì)于undefined,Should無(wú)法為其添加屬性,因此失敗。
通過(guò)node驗(yàn)證發(fā)現(xiàn),導(dǎo)入Should之后,空對(duì)象a增加了一個(gè)should屬性。
> a = {}
> typeof a.should
'undefined'
> require("should")
> typeof a.should
'object'
測(cè)試HTTP接口 - SuperTest
Node.js是用于后端開(kāi)發(fā)的語(yǔ)言,而后端開(kāi)發(fā)其實(shí)很大程度上等價(jià)于編寫(xiě)HTTP接口,為前端提供服務(wù)。那么,Node.js單元測(cè)試則少不了對(duì)HTTP接口進(jìn)行測(cè)試。
我用Node.js自帶的HTTP模塊寫(xiě)了一個(gè)簡(jiǎn)單的HTTP接口server.js
var http = require("http");
var server = http.createServer((req, res) =>
{
res.writeHead(200,
{
"Content-Type": "text/plain"
});
res.end("Hello Fundebug");
});
server.listen(8000);
按照Mocha的原理,測(cè)試HTTP接口并不難: 訪問(wèn)接口; 獲取返回?cái)?shù)據(jù); 驗(yàn)證返回結(jié)果。使用Node.js原生的http與assert模塊就可以了test4.js:
require("../server.js");
var http = require("http");
var assert = require("assert");
it("should return hello fundebug", function(done) {
http.get("http://localhost:8000", function(res) {
res.setEncoding("utf8");
res.on("data", function(text) {
assert.equal(res.statusCode, 200);
assert.equal(text, "Hello Fundebug");
done();
});
});
});
值得稍微注意的一點(diǎn)是,http.get訪問(wèn)HTTP接口是一個(gè)異步操作。Mocha在測(cè)試異步代碼是需要為it函數(shù)添加回調(diào)函數(shù)done,在斷言結(jié)束的地方調(diào)用done,這樣Mocha才能知道什么時(shí)候結(jié)束這個(gè)測(cè)試。
既然Node.js自帶的模塊就能夠測(cè)試HTTP接口了,為什么還需要SuperTest呢?不妨先看一下測(cè)試代碼test5.js:
var request = require("supertest");
var server = require("../server.js");
var assert = require("assert");
it("should return hello fundebug", function(done) {
request(server)
.get("/")
.expect(200)
.expect(function(res) {
assert.equal(res.text, "Hello Fundebug");
})
.end(done);
});
對(duì)比兩個(gè)測(cè)試代碼,會(huì)發(fā)現(xiàn)后者簡(jiǎn)潔很多。
原理
SuperTest封裝了發(fā)送HTTP請(qǐng)求的接口,并且提供了簡(jiǎn)單的expect斷言來(lái)判定接口返回結(jié)果。對(duì)于POST接口,使用SuperTest的優(yōu)勢(shì)將更加明顯,因?yàn)槭褂肗ode.js的http模塊發(fā)送POST請(qǐng)求是很麻煩的。
要做多少單元測(cè)試?
本文所寫(xiě)的單元測(cè)試案例,都很簡(jiǎn)單。然而,在實(shí)際工作中,單元測(cè)試是一個(gè)很頭痛的事情。修改了代碼有時(shí)意味著必須修改單元測(cè)試,寫(xiě)了新的函數(shù)或者API就得寫(xiě)新的單元測(cè)試。如果較真起來(lái),單元測(cè)試可以沒(méi)完沒(méi)了地寫(xiě),但這是沒(méi)有意義的。而根據(jù)二八原理,20%的測(cè)試可以解決80%的問(wèn)題。剩下的20%問(wèn)題,事實(shí)上我們是力不從心的。換句話說(shuō),想通過(guò)測(cè)試消除所有BUG,是不現(xiàn)實(shí)的。
因此,對(duì)生產(chǎn)代碼進(jìn)行實(shí)時(shí)錯(cuò)誤監(jiān)測(cè)是非常有必要的,這也是我們Fundebug努力在做的事情。
參考鏈接
- 測(cè)試框架 Mocha 實(shí)例教程 – 阮一峰
- 單元測(cè)試要做多細(xì) – 酷殼
- 測(cè)試的道理 -王垠
- Pareto principle
本文名稱:玩轉(zhuǎn)Node.js單元測(cè)試
文章路徑:http://www.dlmjj.cn/article/dpsdpjo.html


咨詢
建站咨詢
