玩轉Node.js單元測試

whelshion 7年前發布 | 15K 次閱讀 單元測試 Node.js Node.js 開發

代碼部署之前,進行一定的單元測試是十分必要的,這樣能夠有效并且持續保證代碼質量。而實踐表明,高質量的單元測試還可以幫助我們完善自己的代碼。這篇博客將通過一些簡單的測試案例,介紹幾款Node.js測試模塊: MochaShouldSuperTest 。本文側重于解釋 原理 ,各個模塊的詳細使用案例以后單獨再聊。

為啥需要單元測試?

所謂單元測試,就是對某個函數或者API進行正確性驗證。來看個簡單的例子 add1.js :

function add(a, b)
{
 return a + b;
}

沒錯,我寫了一個加法函數。 這有啥好測的呢? 不妨用node執行一下:

> add = function(a, b){return a + b}
[Function: add]
> add(4)
NaN

當add函數僅給定一個參數4的時候,a為4,b為undefined,兩者相加為NaN。

  • 你考慮過只有一個參數的場景嗎?
  • 給定一個參數時,NaN是你想要的結果嗎?
  • 如果參數不是整數怎么辦?

這時,就需要單元測試來驗證各種可能的場景了。

如果我把add函數定義為 兩個整數相加 ,而其他輸入則返回undefined,那么正確的代碼 add2.js 應該是這樣的:

function add(a, b)
{
 if (typeof a === "number" && typeof b === "number")
 {
 return a + b;
 }
 else
 {
 return undefined;
 }

}

發現一個有趣的現象,我們寫代碼的時候很容易陷入思維漏洞,而寫測試的時候往往會考慮各種情況,這就是所謂的TDD(Test-Driven-Development: 測試驅動開發)的神奇之處。因此, 進行一定的單元測試是十分必要的 :

  • 驗證代碼的正確性
  • 避免修改代碼時出錯
  • 避免其他團隊成員修改代碼時出錯
  • 便于自動化測試與部署

測試框架 - Mocha

下面的測試代碼 test2.js 用于測試 add2.js 。這里使用了測試框架 Mocha 以及Node.js自帶的斷言庫 Assert 。

var add = require("../add2.js");
var assert = require("assert");


// 當2個參數均為整數時
it("should return 3", function()
{
 var sum = add(1, 2);
 assert.equal(sum, 3);
});

// 當第2個參數為String時
it("should return undefined", function()
{
 var sum = add(1, "2");
 assert.equal(sum, undefined);
});

// 當只有1個參數時
it("should return undefined", function()
{
 var sum = add(1);
 assert.equal(sum, undefined);
});

測試代碼中使用了測試框架 Mocha 提供的it函數,3個it函數分別測試了3種不同的案例(test case)。it函數的第1個參數為字符串,用于描述測試,一般會寫期望得到的結果,例如”should return 3”; 而第2個參數為函數,用于編寫測試代碼,一般是先調用被測試的函數或者API,獲取結果之后,使用斷言庫判斷執行結果是否正確。

測試代碼中使用了Node.js自帶的斷言庫 Assert 的 assert.equal 函數,用于判定add函數返回的結果是否正確。assert.equal成功時不會發生什么,而失敗時會拋出一個AssertionError。不妨使用node測試一下:

> 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.<anonymous> (repl.js:513:10)
 at emitOne (events.js:101:20)
 at REPLServer.emit (events.js:188:7)

原理:

我們按照Mocha的it函數編寫一個個測試案例,然后Mocha負責執行這些案例;當assert.equal斷言成功時,則測試案例通過;當assert.equal斷言失敗時,拋出AssertionError,Mocha能夠捕獲到這些異常,然后對應的測試案例失敗。

使用mocha執行test2.js:

mocha test/test2.js

下面為輸出,表示測試案例全部通過

? should return 3
? should return undefined
? should return undefined

3 passing

而當我們使用 test1.js 測試 add1.js 時,則后面2個測試案例失敗:

? 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.<anonymous> (test/test1.js:18:12)

 2) should return undefined:
 AssertionError: NaN == undefined
 at Context.<anonymous> (test/test1.js:25:12)

斷言庫 - Should

Node.js自帶的斷言庫 Assert 提供的函數有限,在實際工作中, Should 等第三方斷言庫則更加強大和實用。

我寫了一個merge函數 merge.js ,實現了類似于 _.extend() 與 Object.assign() 的功能,用于合并兩個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 寫了對應的測試代碼 test3.js :

require("should");
var merge = require("../merge.js");


// 當2個參數均為對象時
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");
});

// 當只有1個參數時
it("should return undefined", function()
{
 var a = {
 name: "Fundebug",
 type: "SaaS"
 };

 var c = merge(a);

 (typeof c).should.equal("undefined");
});

測試代碼稍微有點長,但是使用Should的只有三處:

c.should.have.property("name", "Fundebug");
c.should.have.propertyByPath("product", "frontend").equal("JavaScript");
(typeof c).should.equal("undefined");

可知Should能夠:

  • 驗證對象是否存在某屬性,并驗證其取值
  • 驗證對象是否存在某個嵌套屬性,并使用鏈式方式驗證其取值

那么Should為什么不能直接驗證c的取值為undefined呢?比如這樣寫:

c.should.equal(undefined); // 這樣寫是錯誤的

原理:

Should會為每個對象添加should屬性,然后通過該屬性提供各種斷言函數,我們可以使用這些函數驗證對象的取值。對于undefined,Should無法為其添加屬性,因此失敗。

通過node驗證發現,導入Should之后,空對象a增加了一個should屬性。

> a = {}
> typeof a.should
'undefined'
> require("should")
> typeof a.should
'object'

測試HTTP接口 - SuperTest

Node.js是用于后端開發的語言,而后端開發其實很大程度上等價于編寫HTTP接口,為前端提供服務。那么,Node.js單元測試則少不了對HTTP接口進行測試。

我用Node.js自帶的 HTTP 模塊寫了一個簡單的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的原理,測試HTTP接口并不難: 訪問接口; 獲取返回數據; 驗證返回結果。使用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();
 });
 });
});

值得稍微注意的一點是,http.get訪問HTTP接口是一個異步操作。 Mocha在測試異步代碼是需要為it函數添加回調函數done ,在斷言結束的地方調用done,這樣Mocha才能知道什么時候結束這個測試。

既然Node.js自帶的模塊就能夠測試HTTP接口了,為什么還需要 SuperTest 呢?不妨先看一下測試代碼 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);
});

對比兩個測試代碼,會發現后者簡潔很多。

原理

SuperTest封裝了發送HTTP請求的接口,并且提供了簡單的expect斷言來判定接口返回結果。對于POST接口,使用SuperTest的優勢將更加明顯,因為使用Node.js的http模塊發送POST請求是很麻煩的。

要做多少單元測試?

本文所寫的單元測試案例,都很簡單。然而,在實際工作中,單元測試是一個很頭痛的事情。修改了代碼有時意味著必須修改單元測試,寫了新的函數或者API就得寫新的單元測試。如果較真起來,單元測試可以沒完沒了地寫,但這是沒有意義的。而根據二八原理,20%的測試可以解決80%的問題。剩下的20%問題,事實上我們是力不從心的。換句話說,想通過測試消除所有BUG,是不現實的。

 

 

來自:http://kiwenlau.com/2017/03/20/nodejs-unit-test/

 

 本文由用戶 whelshion 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!