Jest 單元測試 — Bootstrap Modal

Conrad
Conrad KU
Published in
11 min readJun 20, 2023

--

前言

前陣子上線了新供應商系統,在後面維護上發現當初寫的 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。

參考 Bootstrap v5.2.x 官方文件說明

// 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);
});
})

參考文章

--

--

Conrad
Conrad KU

Remember, happiness is a choice, so choose to be happy.