本文 AI 產出,尚未審核

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.mockjest.spyOn 等 API,讓外部依賴(如資料庫、第三方服務)可以輕鬆被替換。
  • 快照測試:對於 API 回傳的 JSON 結構,快照測試可以自動比對變更。
  • 支援 TypeScript:配合 ts-jestbabel-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 只會根據 testMatchtestRegex 找測試檔案。若檔名或目錄不符合規則,測試不會執行。 確認 package.json 中的 testMatch 設為 `/tests//*.test.(ts
TypeScript 設定衝突 tsconfig.json 中的 moduletarget 若與 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 回傳結構穩定且變更頻率低的情況下使用,且在更新快照前務必手動檢查差異。

最佳實踐

  1. 測試分層:先寫單元測試(unit test)驗證純函式或服務層,接著寫整合測試(integration test)驗證路由與資料庫互動,最後視需求加入端到端測試(e2e)。
  2. 保持測試獨立:每個測試案例應該能獨立執行,避免依賴執行順序。
  3. 使用自訂 Jest 設定檔:將設定抽離到 jest.config.js,方便團隊協作與 CI/CD。
  4. 結合 CI:在 GitHub Actions、GitLab CI 等流水線中加入 npm test -- --coverage,確保每次合併前都有測試與覆蓋率報告。
  5. 利用 --watch:開發時使用 npm test -- --watch,Jest 會自動偵測檔案變動並重新執行相關測試,提高迭代速度。

實際應用場景

  1. 新功能開發

    • 開發新 API 前,先在 __tests__ 中寫下預期的輸入與輸出。完成開發後,只要跑一次測試即可驗證功能是否符合需求。
  2. Bug 回歸測試

    • 當收到 bug 回報時,先寫一個失敗的測試案例(Red),再修正程式碼讓測試通過(Green),最後重構(Refactor)。這樣的 TDD 流程能防止同樣的 bug 再次出現。
  3. 持續整合 (CI)

    • 在 CI pipeline 中加入 npm test -- --coverage,若測試失敗或覆蓋率低於門檻,則阻止部署。這保證了每一次的程式碼變更都經過自動驗證。
  4. 模擬外部服務

    • 例如支付平台、郵件服務等第三方 API,使用 jest.mocknock(HTTP mock)在測試中模擬回傳結果,避免在測試環境中真的呼叫外部服務,降低測試成本與不確定性。

總結

Jest 為 Express + TypeScript 專案提供了完整且易上手的測試解決方案。只要透過以下幾個步驟,就能在專案中建立穩固的自動化測試基礎:

  1. 安裝 jestts-jest 與型別檔
  2. package.jsonjest.config.js 中設定 preset: "ts-jest",確保 TypeScript 能被正確編譯。
  3. 撰寫測試檔,使用 describetest(或 it)以及 expect 斷言。
  4. 利用 supertest 測試 Express 路由,不必啟動實體 server。
  5. 善用 jest.mock 進行依賴模擬,讓測試聚焦於業務邏輯。

結合 CI/CD測試分層最佳實踐,即可在開發過程中即時捕捉錯誤、降低回歸風險,最終交付高品質、可維護的 API 服務。祝你在 Jest 的旅程中玩得開心,寫出更可靠的程式碼!