捨棄 create-react-app 之餘還架了個 astro blog 昭告天下:單元測試
寫測試可以幫助看 code 的人(可能是別人、也可能是未來的自己)明確暸解一個功能究竟能接受哪些參數、又會有哪幾種可能的輸出內容,也保障一個功能在「單元測試有覆蓋到的部分」是有品質保證的。為了能在晚上睡得更安穩,能從元件中獨立出來的邏輯就盡量提供配合的單元測試吧。
於是今天的主題就是:如何為白手起家的 React TypeScript app 專案設定 jest ( つ•̀ω•́)つ
安裝套件
在寫這篇鐵人賽文章的當下 jest 版本為 29.6,為了在這版 jest 順利把 TypeScript 測試跑起來,你需要安裝以下套件:
yarn add -D @jest/globals jest ts-jest ts-node
除了 jest
以外都是輔助 TypeScript 用的套件。如果在你閱讀這篇文章時 jest 的版本已遠超 29.6,建議在安裝完 jest
後,根據執行 yarn jest
後的終端訊息一個一個把需要的套件裝回去。
設定 jest.config.ts
在專案根目錄新增一個 jest.config.ts
檔案並填入以下內容:
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'jsdom', // 選填,見下方說明
moduleNameMapper: {
'@/jest.config': '<rootDir>/jest.config.ts',
'@Model/(.*)': '<rootDir>/src/model/$1',
'@Tool/(.*)': '<rootDir>/src/tool/$1',
},
};
export default config;
解說如下:
- 根據官方文件說明,目前有兩種
preset
能讓 jest 能跑 TypeScript 測試,一個是babel
、另外一個就是今天使用的ts-jest
- 在測試腳本中如果有需要使用到瀏覽器環境 api(比如
document.createElement('div')
)的話,記得將testEnvironment
從預設的node
改成jsdom
並安裝套件jest-environment-jsdom
。來源:官方文件 - 想要繼續在單元測試腳本中使用自定義的路徑(而非傳統的相對路徑)時,須設定
moduleNameMapper
讓 jest 知道如何解析自訂路徑- 以上方設定為例,這裡告訴 jest 在讀取到
@/jest.config
路徑時要去取./jest.config.ts
、而在遇到@Model
或@Tool
開頭的路徑時,要去./src/model
或./src/tool
資料夾尋找對應的內容 - 注意這邊是透過 regex syntax 來比對出路徑,
(.*)
比對到的字串會用於$1
,比如@Tool/isValidUser
會對應到<rootDir>/src/tool/isValidUser
.*
代表「任何字元都可以」且「允許長度從零到無限」,加上()
代表要把比對出來的對象 group 起來,然後透過$1
取出被 group 起來的內容
- 以上方設定為例,這裡告訴 jest 在讀取到
在執行 .ts
類型的單元測試時,以上的設定基本上就足夠了。
範例測試
首先在 ./src/tool
中新增了一個檔案 isValidUser.ts
並撰寫以下功能:
export function isValidUser(arg: unknown) {
return (
typeof arg === 'object' && !!arg && 'userName' in arg && !!arg.userName
);
}
接著在 ./src/tool
中新增 isValidUser.test.ts
來撰寫測試腳本:
import { describe, expect, test } from '@jest/globals';
import { isValidUser } from '@Tool/isValidUser';
describe('isValidUser', () => {
test('should return false if the argument is not an object', () => {
expect(isValidUser(null)).toBe(false);
expect(isValidUser(undefined)).toBe(false);
expect(isValidUser(123)).toBe(false);
expect(isValidUser('abc')).toBe(false);
expect(isValidUser(true)).toBe(false);
expect(isValidUser(false)).toBe(false);
});
test('should return false if the argument is an empty object', () => {
expect(isValidUser({})).toBe(false);
});
test('should return false if the argument is an object without userName', () => {
expect(isValidUser({ name: 'user A' })).toBe(false);
});
test('should return false if the argument is an object with empty userName', () => {
expect(isValidUser({ userName: '' })).toBe(false);
});
test('should return true if the argument is an object with non-empty userName', () => {
expect(isValidUser({ userName: 'user A' })).toBe(true);
});
});
(純個人感受:GitHub Copilot 為簡單功能產生測試腳本的表現真的不錯,個人版方案一年 100 美金實在不算太貴,可以考慮課個金)
開啟終端,輸入 make test
(參考第 6 天)即可執行單元測試:
PASS src/tool/isValidUser.test.ts
isValidUser
✓ should return false if the argument is not an object (4 ms)
✓ should return false if the argument is an empty object
✓ should return false if the argument is an object without userName (1 ms)
✓ should return false if the argument is an object with empty userName
✓ should return true if the argument is an object with non-empty userName
單元測試的哲學
個人認為 Good Code, Bad Code 一書第十章「單元測試準則」對測試做了不錯的總結。目前在撰寫單元測試時,我會盡可能讓測試腳本都能符合以下特徵:
- 測試要能確實反映問題,且也不會產生誤報
- 以測試行為為主,不過份涉入實作細節
- 測試失敗時,應能提供具體的錯誤說明(發生在哪裡、哪一個項目失敗、收到的非預期結果又是什麼)
- 當其他工程師閱讀測試內容時,能夠理解「被測試的功能在做什麼」
- 容易被執行,比如透過終端即可觸發、不需要在每次測試時都需要大費周章地進行前置作業
附上個人之前的筆記,有較為細節的摘要,歡迎參考。
總結
對 TypeScript React app 專案設定單元測試的門檻並不高,多一點保險可以改善工程師的睡眠品質,真心推薦。