cover

你有沒有改過一行 code,然後整個 API 壞掉,但你是上線後才發現的?這篇教你用 Jest 寫測試,至少讓這種慘劇在本地就被抓到。接續 上一篇:ESLint + Typings 設定

先講結論

  • Jest + ts-jest + supertest 三件套,Express 路由測試就搞定
  • 設定不難,但 jest.config.ts 有幾個眉角(preset: 'ts-jest' 別忘了)
  • www.ts(伺服器啟動)要不要測?我的建議是:初期不用,把力氣花在路由邏輯上

安裝 Jest

pnpm add -D jest ts-jest @types/jest supertest @types/supertest

然後初始化:

pnpm exec jest --init

選項我建議這樣選:

問題選擇
Use Jest in “test” script?Yes
Use TypeScript for config?Yes
Test environmentnode
Coverage reports?Yes
Coverage providerv8
Auto clear mocks?No

設定 jest.config.ts

初始化完會生一個 jest.config.ts,改成這樣:

import type { Config } from 'jest';
 
const config: Config = {
  clearMocks: true,
  collectCoverage: true,
  collectCoverageFrom: ['src/**/*.ts'],
  coverageDirectory: "coverage",
  coverageProvider: "v8",
  preset: 'ts-jest',
  testEnvironment: 'node',
};
 
export default config;

preset: 'ts-jest' 是關鍵。沒加的話 Jest 看不懂 TypeScript,跑測試會直接噴 syntax error,你會困惑為什麼明明能編譯卻跑不了測試。問我怎麼知道的?別問。

寫測試

建立測試資料夾:

mkdir -p tests/routes

tests/routes/index.test.ts — 測首頁路由:

import request from 'supertest';
import app from '../../src/app';
 
describe('GET /', () => {
  it('should return Welcome to the Home Page', async () => {
    const res = await request(app).get('/');
    expect(res.statusCode).toEqual(200);
    expect(res.text).toContain('Welcome to the Home Page');
  });
});

tests/routes/users.test.ts — 測 users 路由:

import request from 'supertest';
import app from '../../src/app';
 
describe('GET /users', () => {
  it('should return user resource', async () => {
    const res = await request(app).get('/users');
    expect(res.statusCode).toEqual(200);
    expect(res.text).toContain('respond with a resource');
  });
});

注意這裡用的是 supertest,它可以直接把 Express app 丟進去,不用真的起一個 server。這代表你的測試跑起來很快,也不會有 port 衝突的問題。

跑測試

pnpm run test

結果應該長這樣:

PASS  tests/routes/index.test.ts
PASS  tests/routes/users.test.ts

------------|---------|----------|---------|---------|
File        | % Stmts | % Branch | % Funcs | % Lines |
------------|---------|----------|---------|---------|
All files   |   33.58 |    66.66 |       0 |   33.58 |
 app.ts     |     100 |      100 |     100 |     100 |
 www.ts     |       0 |        0 |       0 |       0 |
 routes/    |     100 |      100 |     100 |     100 |
------------|---------|----------|---------|---------|

Test Suites: 2 passed, 2 total
Tests:       2 passed, 2 total

www.ts 要不要測?

你會看到 coverage 報告裡 www.ts 是 0%。這正常嗎?

我覺得正常。www.ts 做的事情就是啟動 server、處理 port 錯誤。要測它的話你得 mock http.createServer、模擬 EADDRINUSE 錯誤… 搞半天測的都是 Node.js 本身的行為,不是你的商業邏輯。

如果你真的很在意 coverage 數字,可以測這三個場景:

  • server 成功啟動
  • port 被佔用(EADDRINUSE
  • 權限不足(EACCES

但我的建議是:先把路由和 middleware 的測試寫好www.ts 的測試等專案長大了再說。測試也要講 CP 值,不是嗎?

最終資料夾結構

├── eslint.config.mjs
├── jest.config.ts
├── package.json
├── src/
│   ├── app.ts
│   ├── bin/www.ts
│   └── routes/
├── tests/
│   └── routes/
│       ├── index.test.ts
│       └── users.test.ts
└── typings/
    └── routes/

到這裡,你的 Express + TypeScript 專案有了 linter、有了型別管理、有了測試。可以說是從「能跑的 prototype」變成了「可以認真開發的 project」了。


沒有測試的 code 就像沒有煞車的車 — 跑得很快,但你不敢轉彎。