在node.js寫測試-mocha+chai斷言庫+supertest模擬連線+sinon替身+nyc統計覆蓋率

Shawn
14 min readJun 2, 2020

--

第一次寫測試,發現也是蠻多要注意的小地方,所以特別寫一篇來記錄,以防自己到時候又忘記了。
底下有些都有附上Github參考,雖然都是我同一個專案的GitHub,但都有特別標記行數,所以點進去會比較容易了解在講甚麼。

如果想進一步了解CI/CD怎麼做,可以看這篇,travis-CI/CD(自動化整合/自動化部署)

mocha測試框架

1. npm install mocha

在專案目錄下安裝mocha套件

2. app.js要module.exports = app

像我專案進入後的主要檔案是app.js,就要記得加上module.exports = app才跑得起來。
GitHub參考

因為在測試檔案那邊有需要把app當作參數傳給supertest這個套件使用,來讓supertest套件發起request時能夠順利跑起來。
GitHub參考

3. mocha生命週期

說mocha生命週期前,先說一下describe,我將之理解為一個一個測試區塊,一個describe底下可以再包多個describe,這裡面的這些describe其實也可以再往下包多it測試,層層往下包,到時候跑測試的結果也是會以多層次來呈現,總之就是看起來怎麼安排那個結構都是可以的。

如下圖,像我Github參考這個就因為
測試 # 登入 — POST /api/signin 底下的
# 帳號或密碼未輸入 裡面
還有 # 帳號未輸入 # 密碼未輸入 兩個狀況要測,
所以就必須在describe底下再包一個describe,接著裡面再寫it。
(裡面一堆Executing那個請先假裝沒看到,這些是跑MySQL的執行指令,但還不知道怎麼解決跑測試的時候,不要讓執行指令跑出來)

回到生命週期我有用到的就這幾個,before、after、 it、beforeEach、afterEach,如下程式碼說明。
其中it就是要測試的內容,至於before、after、beforeEach、afterEach不一定要寫在describe裡,可以拉到最外層寫也是可以的,像我GitHub參考這個就是把每次要去拿到token的before寫到最外面,並沒有寫到describe裡面。

const assert = require('assert')
const chai = require('chai')
const should = chai.should()
describe('Array', function() {
before(function() {
// 在所有測試開始前會執行的程式碼區塊
console.log(' ===== before ===== ')
});
after(function() {
// 在所有測試結束後會執行的程式碼區塊
console.log(' ===== after ===== ')
});
it('給這個測試一個名字', function(done) {

});
beforeEach(function() {
// 在每個 Test Case 開始前執行的程式碼區塊
console.log(' == beforeEach == ')
});
afterEach(function() {
// 在每個 Test Case 結束後執行的程式碼區塊
console.log(' == afterEach == ')
});
});

4. 測試的檔名命名

通常就是在原本的檔案後面加上test,方便辨識,如果寫一支在測userController.js的,這支測試檔案名稱就會用userController.test.js來命名。

5. 寫測試的深度

基本上每個funtion會return幾個結果回來,就要寫幾種測試的可能性。
例如有支API是登入驗證,就return了很多res.json(),這時候測試檔案就要因應測試每個return都會被觸發。
API登入驗證參考
API登入驗證測試檔案參考

6. 測試的結構規劃

測試的資料夾結構就看自己怎麼規劃了,看自己要測試的有哪些東西,必不可少的應該就是controller,如果有seed那就也要測seed。
如下圖,我的測試檔案就是圍繞著controller為主去寫的,看有幾種controller就寫幾個相對應的測試,不過我的資料夾結構有點怪就是了XD,在routes下有個controllers,而seed測試我也還沒時間寫。

7. 輸入mocha tests讓寫好的測試程式跑起來

如果今天直接在專案目錄下的終端機輸入mocha,它會直接去找專案目錄下的test檔案,注意是test資料夾喔!
但因為我把放測試檔案的資料夾命名為複數tests,如果直接輸入mocha就跑不動了,所以必須用mocha tests去跑測試檔案

8. 改用npm run test去跑測試

通常我們都會把要做的事情用npm run xxx指令來跑,所以同樣可以在package.json檔案的scripts加上”test”
--exit表示跑完所有測試就離開
--recursive表示要把tests資料夾下所有測試檔案都跑過(就算tests底下還有子資料夾還是進去跑裡面的測試檔案,所以別擔心會漏掉)
--timeout 5000設定跑每支測試程式等5秒後沒有回應就跳到下一支繼續跑Github參考

// package.json
{
"name": "masungo-backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js",
"test": "mocha tests --exit --recursive --timeout 5000",
"cover": "./node_modules/.bin/nyc mocha tests --recursive --timeout 5000 --exit"
},
"author": "",
"license": "ISC",
"dependencies": {
"bcryptjs": "^2.4.3",
"body-parser": "^1.19.0",
"chai": "^4.2.0",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-session": "^1.17.1",
"faker": "^4.1.0",
"fs": "0.0.1-security",
"jsonwebtoken": "^8.5.1",
"multer": "^1.4.2",
"mysql2": "^2.1.0",
"nodemailer": "^6.4.6",
"passport": "^0.4.1",
"passport-jwt": "^4.0.0",
"pg": "^8.0.3",
"sequelize": "^5.21.6",
"sequelize-cli": "^5.5.1",
"sinon": "^9.0.2",
"supertest": "^4.0.2"
},
"devDependencies": {
"nyc": "^15.0.1"
}
}

chai斷言庫

1. 什麼是斷言庫

這主要是可以讓我們以自己比較熟悉的方式來寫it測試檔案。
chai裝上去後就有多種可選,有should、expect、assert,如下圖三者能夠做同樣的事,但就看自己習慣哪種描述方式。

chai斷言庫官網

2. 如何使用chai

2-1. 安裝chai

npm install chai

2-2. 引入Expect

我用的是Expect,所以這邊就要直接引入Expect,接著就可以在it裡面寫入expect了。

const { expect } = require("chai")

3. Expect常用寫法

其實Expect後面的寫法有很多種,但這邊只記錄有用到的4種而已,而我有全端的專案,也有前後端分離的專案,所以寫法上也稍有不同,底下大致列出說明。

expect(res.statusCode).to.be.equal(200) // 用在測試全端或API都可,用來比對回傳的狀態碼
expect(res.text).to.contain('Register') // 用在測試全端,可以查看傳回去的頁面內容有沒有特定字串,因為寫全端的時候res.render()打出來的資料其實就是整包的HTML了,像如果是註冊頁,就可以查看這整包的文字裡面有沒有Register
expect(res.body.status).to.be.equal('error') // 用在測試API,就是比對看回傳的json資料
expect(res.body.message).to.be.equal("no such user found") // 用在測試API,就是比對看回傳的json資料有沒有要求的字串

supertest

1. 什麼是supertest

如果今天不用這個套件,我們在跑mocha進行測試的時候,就要先npm run dev之類的先把伺服器跑起來,但用了supertest套件之後,每次測試有要發起request給伺服器時,我們不用特地先把伺服器啟動就能跑測試了。

2. 如何使用supertest

2-1. 安裝supertest

npm install supertest

2–2. 引入supertest跟app.js

const request = require("supertest")
const app = require("../../../app.js")

2-3. 在測試檔案裡以request(app)發起request

GitHub參考
it("# 獲取成功", (done) => {
request(app)
.get("/api/getcurrentuser")
.set({
Authorization: `Bearer ${APIToken}`,
})
.end((err, res) => {
expect(res.statusCode).to.be.equal(200)
expect(res.body.user.name).to.be.equal("name")
done()
})
})

sinon替身模擬

1. 什麼是sinon

在打某些API時是要先經過驗證的,例如我這個專案用的就是passport套件去驗證每一次這個人進來這個API是不是有帶正確的JWT給我,接著我才判斷它是有驗證通過的。
這時候sinon的用處就是可以用替身模擬有驗證通過。

其實我sinon只用過一次,因為我只成功用在GET的API,至於POST或PUT用了這種替身寫法就不行了。

2. 如何使用sinon

以下說明案例是模擬GET /api/getcurrentuser,經過驗證已登入就回傳使用者資料。
但如前所述,我在sinon的使用上其實有遇到瓶頸,所以對於需要驗證這個使用者這件事,我是直接在生命週期before的時候先打POST /api/signin,把回傳的JWT存起來給後面幾個測試it用,這樣後面要打其他API且需要攜帶JWT時,我就用剛剛存起來的JWT即可。
因此會看到在sinon的那幾段程式都是暫時註解起來沒在用的,不過拿來解釋sinon的基本操作算是夠用了。

2–1. 把要動手腳(替身模擬)的那個程式用新的函式先封裝起來

這樣才能透過sinon去改要動手腳的那個程式回傳的值,讓它暫時變成我們想要的結果,來假裝模擬已經驗證通過。
說是動手腳也不是真的動手腳,只是在測試的時候暫時假裝驗證過了,所以這樣改一改封裝起來不會影響我們原本程式的正常運行,因為就是多包一層而已。

GitHub參考
// routes/apis.js
// const authenticated = function (req, res, next) {
// passport.authenticate("jwt", { session: false }, (err, user, info) => {
// if (!user) {
// return res
// .status(401)
// .json({ status: "error", message: "No auth token" })
// }
// req.user = user
// return next()
// })(req, res, next)
// }

2-2. 在測試檔案裡透過sinon對目標程式進行替身模擬

下面這段其實就是在對我們剛剛封裝起來的passport.authenticate暫時去動手腳(替身模擬),我們打GET /api/getcurrentuser也就不用帶JWT先做驗證,而是直接讓電腦誤以為目前這個人就是通過驗證了。

GitHub參考
// await db.User.destroy({ where: {}, truncate: { cascade: })
// const rootUser = await db.User.create({ name: "name" })
// this.authenticate = sinon
// .stub(passport, "authenticate")
// .callsFake((strategy, options, callback) => {
// callback(null, { ...rootUser }, null)
// return (req, res, next) => {}
// })
})

nyc統計覆蓋率

1. 什麼是nyc

可以幫我們統計看看到底我們寫完的測試檔案在跑完之後究竟有觸發我們原本的程式哪幾行,進而計算出測試檔案的覆蓋率(完整度)。

2. 如何使用nyc

2-1. 安裝nyc

npm install nyc

2–2. 新增.nycrc檔案

2-2-1. 加入include
覆蓋率比對會只針對include寫的這兩支來比對到底有多少百分比(多少行)的程式在測試的時候被跑過去了。

2-2-2. 加入reporter
可以產出網頁版,跑完之後可以在根目錄下>coverage>index.html用瀏覽器打開看到網頁版的統計報告。

2–2–3. 加入check-coverage
因為統計覆蓋率有分為四種,
行覆蓋率 (line coverage):是否每一行都有執行、
敘述覆蓋率 (statement coverage):是否每個敘述都有執行、
分支覆蓋率 (branch coverage):是否每個 if 條件都有執行、
函數覆蓋率 (function coverage):是否每個函數都有執行。

如果我們都設成10,就表示上述這四種都至少要超過10%才算合格,設定這個參數其實可以用來防止覆蓋率過低的問題。

GitHub參考
// .nycrc
{
"include": [
"controllers/userController.js",
"controllers/api/userController.js"
],
"reporter": [
"text",
"html"
],
"check-coverage": true,
"lines": 10,
"statements": 10,
"functions": 10,
"branches": 10
}

2–3. 用./node_modules/.bin/nyc mocha tests把測試以及統計覆蓋率跑起來

在終端機輸入這個指令就會跑起來了,但這邊有個很重要的,有的人指令的路徑不用.bin,不需要那個.只需要bin,但我確實是要.bin才能啟動nyc,所以大家要自己注意一下喔。
這個卡關無敵久的,最後亂試才成功,網路上好像沒人跟我一樣要這樣用.bin才能啟動的Q"Q

2–4. 改用npm run cover來跑測試

同樣可以在package.json檔案的scripts加上”cover”,以後就不用輸入那麼長的指令去跑測試以及覆蓋率了,用npm run cover即可

GitHub參考
"cover": "./node_modules/.bin/nyc mocha tests --recursive --timeout 5000 --exit"

--

--

Shawn

您好,這是一個中年轉職工程師的大叔的筆記