前言
前陣子上線了新供應商系統,在後面維護上發現當初寫的 Regex Function 令我頭痛,當初每一個 Regex Function 都有自己測試過,但過一天就都忘了,如果當初有把測試過程寫成單元測試的話多美好呀~
也就因為這樣在新的專案想嘗試加上 Jest 測試框架來寫單元測試
接著就跟著我一步一步來踩雷吧
🔖 文章索引
1. 使用 import
2. 使用 TypeScript
3. 開始單元測試
3-1. Step 1. 建立 fn.test.ts
3-2. Step 2. 安裝 jest-environment-jsdom
3-3. Step 3. 使用 describe(name, fn)
3-4. Step 4. test 執行 toggleLoadingModal(true) 觸發 Modal.show()
3-5. Step 5. test 執行 toggleLoadingModal(false) 需等待 setTimeout 500ms
3-6. Step 6. test 執行 toggleLoadingModal(false) 觸發 Modal.hide()
4. 參考文章
使用 import
跟著官方 Get Started 在引入檔案的方式從 require
變成 import
會需要 Babel
來幫忙
$ npm install -D babel-jest @babel/core @babel/preset-env
根目錄創建一個 babel.config.js
module.exports = {
presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
};
使用 TypeScript
新專案已經有 tsconfig.json
所以這邊使用 ts-jest
搭配 @types/jest
$ npm i -D ts-jest @types/jest
產生 jest.config.js
$ npx ts-jest config:init
// jest.config.js
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node'
};
開始單元測試
接下來要進行單元測試的函式 ( 如下 ) 看起來很簡單的函式在寫測試時陷入困境 😢
// wwwroot/ts/utilities/toggleLoadingModal.ts
import * as bootstrap from "bootstrap";
const loadingModal = new bootstrap.Modal('#loadingModal');
export default function(isOpen: boolean) {
if (isOpen) {
loadingModal.show();
} else {
setTimeout(function() {
loadingModal.hide();
}, 500);
}
}
Step 1. 建立 fn.test.ts
函式名 toggleLoadingModal.ts
建立測試時名稱為 toggleLoadingModal.test.ts
將 toggleLoadingModal.test.ts
建立在 wwwroot/__test__/
資料夾中
Step 2. 安裝 jest-environment-jsdom
// wwwroot/__test__/toggleLoadingModal.test.ts
import toggleLoadingModal from "../ts/utilities/toggleLoadingModal";
test("test", () => {
toggleLoadingModal(true);
});
// package.json
{
...
"scripts": {
"test": "jest"
}
...
}
當執行 npm run test
後會產生錯誤訊息需要安裝 jest-environment-jsdom
安裝後 npm scripts
需修改成 "test": "jest --env=jsdom"
再次執行會產生錯誤訊息 TypeError: Cannot read properties of undefined ( reading 'backdrop' )
這是因為 new bootstrap.Modal('#loadingModal')
找不到 #loadingModal
// wwwroot/__test__/toggleLoadingModal.test.ts
// 加上 DOM
document.body.innerHTML = `
<div id="loadingModal" class="modal fade" data-bs-backdrop="static" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content bg-transparent border-0">
<div class="modal-body text-center">
<img src="~/images/loading.gif" alt="">
</div>
</div>
</div>
</div>
`;
import toggleLoadingModal from "../ts/utilities/toggleLoadingModal";
test("test", () => {
toggleLoadingModal(true);
});
再次執行 npm run test
就會通過囉
Step 3. 使用 describe(name, fn)
將相關的測試組合起來
// wwwroot/__test__/toggleLoadingModal.test.ts
// 加上 DOM
document.body.innerHTML = `
<div id="loadingModal" class="modal fade" data-bs-backdrop="static" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content bg-transparent border-0">
<div class="modal-body text-center">
<img src="~/images/loading.gif" alt="">
</div>
</div>
</div>
</div>
`;
import toggleLoadingModal from "../ts/utilities/toggleLoadingModal";
describe("開始測試 toggleLoadingModal(Boolean)", () => {
test("執行 toggleLoadingModal(true) 觸發 Modal.show()", (done) => {
...
});
test("執行 toggleLoadingModal(false) 需等待 setTimeout 500ms", (done) => {
...
});
test("執行 toggleLoadingModal(false) 觸發 Modal.hide()", () => {
...
});
})
Step 4. test 執行 toggleLoadingModal(true) 觸發 Modal.show()
這邊會使用到 setTimeout
等到打開 Modal 動畫結束才會進行 expect...
因為用了 setTimeout
會需要用 done
來告知 jest 哪邊才是結束的地方
記得 try...catch...
不然會多等 5秒還會多顯示 timeout 錯誤訊息
參考文章:Jest 非同步測試
// wwwroot/__test__/toggleLoadingModal.test.ts
...
describe("開始測試 toggleLoadingModal(Boolean)", () => {
test("執行 toggleLoadingModal(true) 觸發 Modal.show()", (done) => {
// Arrange
const loadingModal = document.getElementById('loadingModal') as HTMLDivElement;
let hasShowModal = false;
const expected = true;
// Act
toggleLoadingModal(true);
// Assert
// 使用 setTimeout 模擬開啟 modal 的動畫時間
setTimeout(function () {
hasShowModal = loadingModal.classList.contains('show');
try {
expect(hasShowModal).toBe(expected);
done();
} catch (err) {
done(err);
}
}, 500);
});
})
Step 5. test 執行 toggleLoadingModal(false) 需等待 setTimeout 500ms
這裡會特別用 setTimeout
是因為在 Local 測試 Call API 回傳 status 400 時因為來回秒數太短,會造成開啟 loadingModal 動畫還在跑的時候就執行關閉 loadingModal 導致被忽略所以就關不了 loadingModal。
// wwwroot/__test__/toggleLoadingModal.test.ts
...
describe("開始測試 toggleLoadingModal(Boolean)", () => {
...
test("執行 toggleLoadingModal(false) 需等待 setTimeout 500ms", (done) => {
// Arrange
jest.useFakeTimers();
jest.spyOn(global, 'setTimeout');
// Act
toggleLoadingModal(false);
// Assert
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 500);
});
});
Step 6. test 執行 toggleLoadingModal(false) 觸發 Modal.hide()
因為 toggleLoadingModal(false)
有 setTimeout
會需要使用到 jest.useFakeTimers()
和 jest.runAllTimers()
// wwwroot/__test__/toggleLoadingModal.test.ts
...
describe("開始測試 toggleLoadingModal(Boolean)", () => {
...
test("執行 toggleLoadingModal(false) 觸發 Modal.hide()", () => {
// Arrange
const loadingModal = document.getElementById('loadingModal') as HTMLDivElement;
let hasShowModal = true;
const expected = false;
loadingModal.classList.add('show');
// Act
jest.useFakeTimers();
toggleLoadingModal(false);
jest.runAllTimers();
hasShowModal = loadingModal.classList.contains('show');
// Assert
expect(hasShowModal).toBe(expected);
});
})