之前寫測試因為沒有注意細節,導致非常難編寫單元測試;
改以 End-to-End測試,直接用docker 開DB輸入假資料,接著執行 Server App 對API一隻一隻測試。
這樣的好處是測試方法與最終API調用的結果是一樣的,但缺點就是耗時較久,且邊寫測試的成本很高,要做到TDD之類的開發方式非常困難。
另一方面之前用 Mocha,也算是 Nodejs 最大宗使用的測試框架,提供最基本的測試環境與語法定義,其餘的斷言、Mocking都要自己另外想辦法;
這次改用jest,由FB開源可供前後端的測試框架,此次順便分享如何建構程式碼才方便寫測試的小小心得。
Jest 如何使用
Jest 使用上有點像 Mocha + Chai,除了測試環境外還以自定義的斷言,提供多種編寫語意化的測試情境。
1
2
3
4
5
6
7
8
9
10
11
| // ./sum.js
function sum(a, b) {
return a + b;
}
module.exports = sum;
// ./sum.test.js
const sum = require(‘./sum’);
test(‘adds 1 + 2 to equal 3’, () => {
expect(sum(1, 2)).toBe(3);
});
|
// 執行
> jest
另一點方便的是 Jest對於 Callback、Promise、Async/Await支援相當好,當初在Mocha中搞了一陣子….
邊寫測試的小細節
使用Mock,透過預設情景與假資料,不用架設複雜的環境或是真的發出非同步請求,就可以達到測試的效果;
在使用Mock之前,有一點要注意 盡量把API包成函式或整理成物件方法而非直接呼叫
例如說在設計API: GET /user/:userId
1
2
3
4
5
6
7
8
9
| // 不要這樣直接呼叫 mongoose 方法
function getUserById(){
let user = await mongoose.findById(….)
}// 拆開成獨立的函式,方便後續做 Mock
function getUserById(){
let user = await getUserByIdInModel(id)
}function getUserByIdInModel(){
return mongoose.findById(…)
}
|
將非同步請求整理包成函式在調用有幾個好處
1. 解耦,多一層抽象化
試想今天是用MongoDB當作資料庫,但如果某天需要換成 PostgreSQL,把資料庫調用寫死在 API邏輯中會導致整份程式碼需要重寫;
但如果拆開成獨立函式,就只要改函式調用的DB Client與對應原本的回傳資料格式即可(親身經驗
2. 方便編寫測試
非同步請求本身也可以獨立寫測試 (如果需要的話
接著看 Jest如何生成假資料。
Mock
產生假資料可以分為 func
假設我們原本的程式碼為
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // index.js
const api = require(‘./apiWrapper’);
const Obj = require(‘./objectWrapper’);exports.callAPI = async function(t){
let content = await api.readFile(t)
return content
}exports.callObject = async function(){
let obj = new Obj(“jay”)
return obj.sayHi()
}
// ./util/apiWrapper.js
const fs = require(‘mz/fs’);exports.readFile = async function(params){
return fs.readFile(“./test.txt”);
}
// ./model/objWrapper.js
module.exports = class Test{
constructor(name){
this.name = name
}sayHi(){
return \`hi from ${this.name}\`
}
}
|
基本上就是index.js對外輸出兩個函式,此兩個函式內有非同步請求與創建物件的方法,通常個人資料夾目錄是共用的非同步API Call會叫做 util,如果是資料庫相關的物件則是 model。
接著看測試檔 index.test.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| const index = require(“./index”)
jest.mock(“./util/apiWrapper”)
jest.mock(“./model/objectWrapper”)
test(“mock api test”, async ()=>{
let res = await index.callAPI(1);
expect(res).toBe(“jest”);
})
test(“mock api test2”, async ()=>{
let res = await index.callAPI(2);
expect(res).toBe(“jest2”);
})
test(“mock obj test”, async ()=>{
let res = await index.callObject();
expect(res).toBe(“hi from jest”);
})
|
這裡先定義了三個測試情境,並透過 jest.mock()顯式宣告 Mock檔案的位置;
接著注意 mock預設有固定的資料路徑與命名規則。
1
2
3
4
5
6
| index.js
index.test.js
— util
| — — apiWrapper.js
| — — __mocks__
| — — apiWrapper.js
|
需注意mock要定義在檔案的同層 mocks 資料夾下,並且檔名需要一致且輸出同樣的函式名。
如果是要 mock 如 ‘fs’等原生模組,直接定義在專案最上層的 __mocks__中即可。
在 jest.fn()中,有需多 mock資料的方式
1. mockImplementation(fn),可以取得函式調用的參數,並決定如何回傳
1
2
3
4
5
6
7
| // ./util/__mocks__/apiWrapper.js
exports.readFile = jest.fn().mockImplementation(params => {
if(params == 1){
return “jest”
}
return “jest2”
})
|
2. mockReturnValueOnce 一次性的回傳值
1
2
3
| jest.fn()
.mockReturnValueOnce(10)
.mockReturnValueOnce(‘x’)
|
3. mockReturnValue 固定的回傳值
有定義並宣告使用 mock,jest在執行原腳本時會改呼叫被 mock覆寫定義的函式 jest.fn(),達到產生假資料的效果,同時紀錄 fn()呼叫的次數與傳遞的參數;
物件則同樣也是透過改寫物件的呼叫方法。
其餘的在程式碼中,https://github.com/sj82516/very-simple-jest-example