本文 AI 產出,尚未審核

ExpressJS (TypeScript) – Testing 自動化測試:Supertest API 測試


簡介

Node.js 生態系中,Express 是最常見的 Web 框架,而使用 TypeScript 開發則能提供編譯時的型別安全,減少執行時錯誤。開發完 API 後,如果沒有自動化測試,任何微小的變更都可能導致不可預期的回應,甚至破壞上線服務。
Supertest 是一套與 Superagent 配合的測試工具,專門用來對 Express (或任何符合 Connect 標準的) 應用程式發送 HTTP 請求,並以 Jest / Mocha 等測試框架斷言回應結果。透過 Supertest,我們可以在 單元測試整合測試 甚至 CI/CD 流程中,快速驗證路由、驗證中介層、錯誤處理等行為是否如預期。

本篇文章將以 TypeScript 為基礎,示範如何使用 Supertest 針對 Express API 撰寫可讀、可維護且易於擴充的自動化測試。


核心概念

1️⃣ 設定測試環境

在測試前,我們需要先把 Express 應用程式 以可被 Supertest 直接呼叫的方式匯出,而不是在 app.listen() 內直接啟動伺服器。這樣可以避免測試時真的開啟埠口。

// src/app.ts
import express from 'express';
import userRouter from './routes/user';

const app = express();

app.use(express.json());
app.use('/api/users', userRouter);

// 只在主程式執行時才監聽
if (require.main === module) {
  const PORT = process.env.PORT || 3000;
  app.listen(PORT, () => console.log(`Server listening on ${PORT}`));
}

export default app;   // <-- 讓測試檔案可以 import

重點export default app 讓測試檔案可以直接以 request(app) 的方式發送請求,避免產生多個執行緒或端口衝突。


2️⃣ 基本的 Supertest 使用方式

下面示範最簡單的 GET 請求測試,搭配 Jest 作為測試 runner。

// tests/user.test.ts
import request from 'supertest';
import app from '../src/app';

describe('GET /api/users', () => {
  it('should return an empty array when no users exist', async () => {
    const response = await request(app).get('/api/users').expect(200);
    expect(response.body).toEqual([]);
  });
});
  • request(app):傳入 Express 實例。
  • .get('/api/users'):發送 GET 請求。
  • .expect(200):直接斷言 HTTP 狀態碼。
  • expect(response.body).toEqual([]):使用 Jest 的斷言檢查回傳資料。

3️⃣ 測試 POST、PUT、DELETE 以及驗證請求 Body

以下範例展示如何測試 建立使用者(POST)以及 更新使用者(PUT)的流程,並同時檢查驗證錯誤。

// tests/user.test.ts(續)
describe('POST /api/users', () => {
  it('should create a new user and return it', async () => {
    const newUser = { name: 'Alice', email: 'alice@example.com' };
    const response = await request(app)
      .post('/api/users')
      .send(newUser)               // <-- 送出 JSON body
      .set('Accept', 'application/json')
      .expect('Content-Type', /json/)
      .expect(201);                // 建立成功的 HTTP 狀態碼

    expect(response.body).toMatchObject(newUser);
    expect(response.body).toHaveProperty('id'); // 伺服器會自動產生 id
  });

  it('should reject when required fields are missing', async () => {
    const incompleteUser = { name: 'Bob' }; // 缺少 email
    const response = await request(app)
      .post('/api/users')
      .send(incompleteUser)
      .expect(400);

    expect(response.body).toHaveProperty('error');
    expect(response.body.error).toContain('email');
  });
});

技巧

  • 使用 .set('Accept', 'application/json') 明確告訴伺服器我們期望 JSON 回應。
  • .expect('Content-Type', /json/) 可以同時驗證回應的 Content-Type
  • toMatchObject 只比對部份屬性,適合檢查自動產生的 id

4️⃣ 模擬資料庫(Mock)與測試隔離

在測試環境下,我們不希望真的寫入正式資料庫。常見做法是 使用 In‑Memory DB(如 sqlite3)或 mock DAO/Repository。以下示範如何使用 jest.mock 取代原本的 UserService

// src/services/userService.ts
export class UserService {
  async create(user: { name: string; email: string }) {
    // 假設此方法會寫入資料庫
    // ...
    return { id: 1, ...user };
  }
}

// tests/user.test.ts(續)
import { UserService } from '../src/services/userService';
jest.mock('../src/services/userService'); // <-- 自動產生 mock

const mockedCreate = jest.fn().mockResolvedValue({
  id: 99,
  name: 'Mocked User',
  email: 'mock@example.com',
});
(UserService as jest.Mock).mockImplementation(() => ({
  create: mockedCreate,
}));

describe('POST /api/users with mocked service', () => {
  it('should use the mocked service and return mocked data', async () => {
    const payload = { name: 'Test', email: 'test@example.com' };
    const res = await request(app).post('/api/users').send(payload).expect(201);
    expect(mockedCreate).toHaveBeenCalledWith(payload);
    expect(res.body.id).toBe(99);
  });
});

透過 mock,測試只關注 API 行為,不會因為資料庫連線失敗而影響測試結果,亦能提升測試速度。


5️⃣ 結合測試前置與清理(Hooks)

使用 Jest 的 beforeAll, beforeEach, afterAll 等 hook,可以在測試執行前後做 資料初始化關閉連線 等工作,確保每個測試案例都是 獨立 的。

// tests/user.test.ts(續)
let server: import('http').Server;

beforeAll(() => {
  // 若有需要額外啟動監聽埠口,可在此處
  server = app.listen(0); // 0 代表自動分配埠口
});

afterAll(async () => {
  await server.close(); // 釋放資源
});

describe('DELETE /api/users/:id', () => {
  it('should delete an existing user', async () => {
    // 先建立一筆資料(使用 mock 或直接呼叫 service)
    const created = await request(app)
      .post('/api/users')
      .send({ name: 'Del', email: 'del@example.com' });

    const id = created.body.id;
    await request(app).delete(`/api/users/${id}`).expect(204);
  });
});

常見陷阱與最佳實踐

陷阱 說明 改善方式
測試時直接使用 app.listen() 會導致埠口被佔用,測試跑不下去。 只在主程式 (if (require.main === module)) 呼叫 listen,測試時直接 import app
未清理測試資料 前一次測試留下的資料會影響後續測試結果,產生間歇性失敗。 使用 beforeEach / afterEach 重置資料庫或 mock,或採用 transaction rollback
忘記設定 Content-Type 某些中介層只接受 application/json,測試會得到 415 錯誤。 request(app).post(...).set('Accept', 'application/json') 中明確設定。
直接依賴實體服務 連接真實 DB、外部 API 時測試慢且不穩定。 Mock 服務或使用 in‑memory DB,只測試 API 本身的行為。
斷言過於寬鬆 只檢查狀態碼,錯誤訊息或資料結構變更不易被捕捉。 同時斷言 status, headers, body 結構(如 toMatchObjecttoHaveProperty)。

最佳實踐

  1. 保持測試獨立:每個 it 應該不依賴其他測試的執行結果。
  2. 使用 TypeScript 型別:在測試檔案中也寫入介面 (interface UserResponse { id: number; name: string; email: string; }) 讓編譯器幫忙檢查回傳結構。
  3. 結合 CI:把 npm test -- --runInBand 加入 GitHub Actions,確保每一次 PR 都會跑完整測試。
  4. 測試覆蓋率:使用 jest --coverage 觀察哪些路由尚未被測試,持續提升覆蓋率。
  5. 文件化測試案例:在 README 或 Confluence 中列出每個 API 的測試目的,方便新人快速了解測試範圍。

實際應用場景

  1. 微服務間契約測試
    在多服務架構下,A 服務的某個 API 需要被 B 服務呼叫。利用 Supertest 在 A 服務的 CI 中跑完整的 契約測試,確保回傳格式不會因為重構而破壞 B 服務。

  2. 驗證授權與中介層
    透過 Supertest 可以在同一測試中加入 JWTOAuth 的 Header,驗證未授權、過期 token、權限不足的情況。例如:

    request(app)
      .get('/api/admin')
      .set('Authorization', `Bearer ${invalidToken}`)
      .expect(401);
    
  3. 端到端(E2E)測試
    雖然 Supertest 仍屬於 API 層 測試,但結合 PuppeteerPlaywright,可在同一 CI pipeline 中先跑 API 測試,確保後端正常後再執行前端 UI 測試,縮短除錯時間。

  4. 資料遷移與版本升級
    當 API 需要升級至新版本(v1 → v2)時,先寫好 舊版與新版的測試,確保新實作仍能兼容舊有客戶端,降低上線風險。


總結

  • Supertest 為 Express(含 TypeScript)提供了簡潔且功能完整的 HTTP 測試介面,讓我們能在 單元測試整合測試 階段即時驗證 API 行為。
  • 透過 app 匯出Jest hooksmock 服務型別斷言,可以建立 快速、可靠且易於維護 的測試基礎。
  • 注意避免端口衝突、資料污染與過度依賴實體服務,遵循 測試獨立、斷言完整、CI 整合 的最佳實踐,才能在實務專案中真正發揮自動化測試的價值。

掌握上述概念與技巧後,你的 Express + TypeScript 專案將能在每一次提交、每一次部署時,都自動得到 安全感品質保證。祝你測試順利,開發愉快! 🚀