ExpressJS (TypeScript) – 單元:Testing 自動化測試
主題:Jest 基礎設定
簡介
在 Node.js 生態系中,Express 是最常被使用的 Web 框架,而 TypeScript 則為大型專案提供了靜態型別檢查與更好的開發體驗。當我們在開發 API 時,若缺乏可靠的測試,程式碼的變更很容易引入 regressions,最終導致服務不穩定或是客戶端錯誤。
Jest 是 Facebook 旗下開源的測試框架,支援單元測試、整合測試與快照測試,且內建 TypeScript 支援、模擬(mock)功能與併行執行等特性,非常適合作為 Express + TypeScript 專案的測試工具。本文將一步步帶你完成 Jest 的基礎設定,並提供實作範例,讓你能在專案中快速上手自動化測試。
核心概念
1. 為什麼選擇 Jest?
- 零設定:安裝後即可以
npm test執行測試,無需額外的測試跑者(test runner)或斷言庫(assertion library)。 - 內建模擬:提供
jest.mock、jest.spyOn等 API,讓外部依賴(如資料庫、第三方服務)可以輕鬆被替換。 - 快照測試:對於 API 回傳的 JSON 結構,快照測試可以自動比對變更。
- 支援 TypeScript:配合
ts-jest或babel-jest,即可直接執行.ts檔案。
2. 安裝與基本設定
在 Express + TypeScript 專案中,我們通常會安裝以下套件:
npm install --save-dev jest ts-jest @types/jest
jest:測試框架本體。ts-jest:Jest 的 TypeScript 前置處理器,負責把 TypeScript 轉譯成 JavaScript。@types/jest:提供 TypeScript 的型別宣告,讓編輯器能正確補全 Jest API。
接著在 package.json 中加入測試腳本與 Jest 設定:
{
"scripts": {
"test": "jest"
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "node",
"moduleFileExtensions": ["ts", "js", "json"],
"rootDir": "src",
"testMatch": ["**/__tests__/**/*.test.(ts|js)"],
"coverageDirectory": "../coverage",
"collectCoverageFrom": ["**/*.{ts,js}", "!**/node_modules/**"]
}
}
重點:
preset: "ts-jest"讓 Jest 在執行測試前自動使用ts-jest轉譯 TypeScript;testEnvironment: "node"表示測試會在 Node 環境下執行,適合測試 Express 路由或服務層。
3. 撰寫第一個測試檔
以下示範一個簡單的 Calculator 模組與其測試。先在 src/utils/calculator.ts 建立模組:
// src/utils/calculator.ts
export class Calculator {
/** 計算兩個數字的加總 */
static add(a: number, b: number): number {
return a + b;
}
/** 計算兩個數字的減法 */
static subtract(a: number, b: number): number {
return a - b;
}
}
接著在 src/__tests__/calculator.test.ts 撰寫測試:
// src/__tests__/calculator.test.ts
import { Calculator } from '../utils/calculator';
describe('Calculator 基本運算', () => {
test('add() 應回傳正確的加總', () => {
expect(Calculator.add(2, 3)).toBe(5);
});
test('subtract() 應回傳正確的差值', () => {
expect(Calculator.subtract(10, 4)).toBe(6);
});
});
執行 npm test 後,你會看到兩個測試都 passed。這就是 Jest 的最小可運行範例。
4. 測試 Express 路由
下面示範如何測試一個簡單的 Express API。假設有以下路由檔案:
// src/routes/user.ts
import { Router, Request, Response } from 'express';
const router = Router();
router.get('/users/:id', (req: Request, res: Response) => {
const { id } = req.params;
// 假設從資料庫取得使用者資訊
const user = { id, name: 'John Doe' };
res.json(user);
});
export default router;
為了測試這個路由,我們會使用 supertest(一個專門測試 HTTP server 的套件):
npm install --save-dev supertest @types/supertest
測試程式如下:
// src/__tests__/user.route.test.ts
import request from 'supertest';
import express from 'express';
import userRouter from '../routes/user';
const app = express();
app.use(express.json());
app.use(userRouter);
describe('GET /users/:id', () => {
it('應回傳對應的使用者資料', async () => {
const response = await request(app).get('/users/123');
expect(response.status).toBe(200);
expect(response.body).toEqual({ id: '123', name: 'John Doe' });
});
});
小技巧:直接在測試檔內建立
express()實例,而不必啟動實體的 server,這樣測試速度更快且不會佔用埠口。
5. 使用 Jest Mock 模擬外部依賴
在真實專案中,API 可能會呼叫資料庫、Redis 或第三方服務。以下示範如何 mock 一個資料庫模組:
// src/services/user.service.ts
import { db } from '../db';
export async function getUserById(id: string) {
const row = await db.query('SELECT * FROM users WHERE id = $1', [id]);
return row[0];
}
測試時,我們不想真的連到資料庫,只需要模擬 db.query 的回傳值:
// src/__tests__/user.service.test.ts
import { getUserById } from '../services/user.service';
import { db } from '../db';
jest.mock('../db'); // 告訴 Jest 把整個模組換成 mock
const mockedDb = db as jest.Mocked<typeof db>;
describe('getUserById', () => {
it('應回傳正確的使用者物件', async () => {
// 設定 mock 的回傳值
mockedDb.query.mockResolvedValueOnce([{ id: '1', name: 'Alice' }]);
const user = await getUserById('1');
expect(user).toEqual({ id: '1', name: 'Alice' });
expect(mockedDb.query).toHaveBeenCalledWith(
'SELECT * FROM users WHERE id = $1',
['1']
);
});
});
透過 jest.mock 我們可以在測試中 完全隔離 外部資源,只關注業務邏輯本身。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 測試檔案未被偵測 | Jest 只會根據 testMatch 或 testRegex 找測試檔案。若檔名或目錄不符合規則,測試不會執行。 |
確認 package.json 中的 testMatch 設為 `/tests//*.test.(ts |
| TypeScript 設定衝突 | tsconfig.json 中的 module、target 若與 ts-jest 不相容,會出現編譯錯誤。 |
使用 ts-jest 建議的 tsconfig("module": "commonjs"、"esModuleInterop": true),或在 jest 中透過 globals: { "ts-jest": { tsconfig: "./tsconfig.json" }} 明確指定。 |
| 模擬未正確還原 | 測試結束後未還原 mock,導致後續測試受到污染。 | 使用 jest.clearAllMocks()、jest.resetAllMocks() 或在 afterEach 中呼叫 jest.restoreAllMocks()。 |
| 非同步測試忘記回傳 Promise | async 測試內若未 await,測試會提前結束,結果不可靠。 |
確保每個 it/test 都回傳 Promise(使用 async/await 或直接回傳 Promise)。 |
| 快照測試過度依賴 | 盲目使用快照會讓測試變成「只要點接受」的流程,失去意義。 | 僅在 API 回傳結構穩定且變更頻率低的情況下使用,且在更新快照前務必手動檢查差異。 |
最佳實踐:
- 測試分層:先寫單元測試(unit test)驗證純函式或服務層,接著寫整合測試(integration test)驗證路由與資料庫互動,最後視需求加入端到端測試(e2e)。
- 保持測試獨立:每個測試案例應該能獨立執行,避免依賴執行順序。
- 使用自訂 Jest 設定檔:將設定抽離到
jest.config.js,方便團隊協作與 CI/CD。 - 結合 CI:在 GitHub Actions、GitLab CI 等流水線中加入
npm test -- --coverage,確保每次合併前都有測試與覆蓋率報告。 - 利用
--watch:開發時使用npm test -- --watch,Jest 會自動偵測檔案變動並重新執行相關測試,提高迭代速度。
實際應用場景
新功能開發
- 開發新 API 前,先在
__tests__中寫下預期的輸入與輸出。完成開發後,只要跑一次測試即可驗證功能是否符合需求。
- 開發新 API 前,先在
Bug 回歸測試
- 當收到 bug 回報時,先寫一個失敗的測試案例(Red),再修正程式碼讓測試通過(Green),最後重構(Refactor)。這樣的 TDD 流程能防止同樣的 bug 再次出現。
持續整合 (CI)
- 在 CI pipeline 中加入
npm test -- --coverage,若測試失敗或覆蓋率低於門檻,則阻止部署。這保證了每一次的程式碼變更都經過自動驗證。
- 在 CI pipeline 中加入
模擬外部服務
- 例如支付平台、郵件服務等第三方 API,使用
jest.mock或nock(HTTP mock)在測試中模擬回傳結果,避免在測試環境中真的呼叫外部服務,降低測試成本與不確定性。
- 例如支付平台、郵件服務等第三方 API,使用
總結
Jest 為 Express + TypeScript 專案提供了完整且易上手的測試解決方案。只要透過以下幾個步驟,就能在專案中建立穩固的自動化測試基礎:
- 安裝
jest、ts-jest與型別檔。 - 在
package.json或jest.config.js中設定preset: "ts-jest",確保 TypeScript 能被正確編譯。 - 撰寫測試檔,使用
describe、test(或it)以及expect斷言。 - 利用
supertest測試 Express 路由,不必啟動實體 server。 - 善用
jest.mock進行依賴模擬,讓測試聚焦於業務邏輯。
結合 CI/CD、測試分層 與 最佳實踐,即可在開發過程中即時捕捉錯誤、降低回歸風險,最終交付高品質、可維護的 API 服務。祝你在 Jest 的旅程中玩得開心,寫出更可靠的程式碼!