玩轉 Node.js 單元測試
程式碼部署之前,進行一定的單元測試是十分必要的,這樣能夠有效並且持續保證程式碼質量。而實踐表明,高質量的單元測試還可以幫助我們完善自己的程式碼。這篇部落格將通過一些簡單的測試案例,介紹幾款Node.js測試模組: Mocha和Should,SuperTest。本文側重於解釋原理,各個模組的詳細使用案例以後單獨再聊。
為啥需要單元測試?
所謂單元測試,就是對某個函式或者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,是不現實的。
因此,對生產程式碼進行實時錯誤監測是非常有必要的,這也是我們Fundebug努力在做的事情。
參考連結
相關文章
- 玩轉 Github Profile Readme:單元測試Github
- Node.js 單元測試:workflowNode.js
- 一起玩轉微服務(14)——單元測試微服務
- 測試 之Java單元測試、Android單元測試JavaAndroid
- 單元測試:單元測試中的mockMock
- 玩轉Google開源C++單元測試框架Google Test系列(gtest)(總)GoC++框架
- [iOS單元測試系列]單元測試編碼規範iOS
- Flutter 單元測試Flutter
- Go單元測試Go
- 單元測試工具
- iOS 單元測試iOS
- 前端單元測試前端
- golang 單元測試Golang
- PHP 單元測試PHP
- phpunit單元測試PHP
- JUnit單元測試
- unittest單元測試
- Junit 單元測試.
- 單元測試真
- 用Junit Framework編寫單元測試 (轉)Framework
- 前端測試:Part II (單元測試)前端
- JavaScript單元測試框架JavaScript框架
- React元件單元測試React元件
- 聊聊前端單元測試前端
- Google 單元測試框架Go框架
- 單元測試 -- mocha + chaiAI
- 單元測試與MockitoMockito
- 單元測試基礎
- Vue單元測試探索Vue
- 單元測試與 PowerMockMock
- junit-單元測試
- Android - 單元測試Android
- 單元測試理解· 1
- 單元測試學習
- android單元測試Android
- [Android] 單元測試Android
- Xcode 單元測試XCode
- JUnit 4 單元測試