5.3 測試替身與 API 測試#
問題:測試依賴外部世界#
測試一個「查資料庫回傳使用者」的函式,問題來了:
- 要真的開一個資料庫嗎?太慢、太麻煩
- 測試裡呼叫外部 API?結果不可控,還要花錢
解法:測試替身(Test Doubles / Mock)——用假的依賴取代真的,讓測試只專注於被測函式本身的邏輯。
Mock:取代外部依賴#
假設被測程式依賴一個取得使用者的服務:
// user.js
export async function getWelcomeMessage(fetchUser, userId) {
const user = await fetchUser(userId);
return `歡迎, ${user.name}`;
}
測試時傳入假的 fetchUser,不碰任何真實資料庫:
import { test } from "node:test";
import assert from "node:assert";
import { getWelcomeMessage } from "./user.js";
test("應該組出歡迎訊息", async () => {
const fakeFetchUser = async (id) => ({ id, name: "小明" });
const msg = await getWelcomeMessage(fakeFetchUser, 1);
assert.equal(msg, "歡迎, 小明");
});
核心觀念:依賴用參數傳入(依賴注入),測試就能換成假的。這也是好設計——函式不內部偷渡依賴。
node:test 的 mock 功能#
import { test, mock } from "node:test";
test("sendEmail 應該被呼叫一次", async () => {
const sendEmail = mock.fn(async () => true);
await notifyUser(sendEmail, { id: 1, name: "小明" });
assert.equal(sendEmail.mock.callCount(), 1); // 被呼叫了幾次
assert.deepEqual(sendEmail.mock.calls[0].arguments, // 呼叫時傳了什麼
[{ id: 1, name: "小明" }]);
});
API 測試:不開真的伺服器#
用 supertest 直接對 Express app 發請求:
npm install -D supertest
把 Express app 拆成兩個檔案(測試的關鍵前置工作):
// app.js —— 只匯出 app,不 listen
import express from "express";
const app = express();
app.use(express.json());
app.get("/api/todos", (req, res) => res.json([{ id: 1, title: "學 Node" }]));
export default app;
// server.js —— 只負責啟動
import app from "./app.js";
app.listen(process.env.PORT || 3000);
測試 app.test.js:
import { test } from "node:test";
import assert from "node:assert";
import request from "supertest";
import app from "./app.js";
test("GET /api/todos 應該回傳清單", async () => {
const res = await request(app).get("/api/todos");
assert.equal(res.status, 200);
assert.equal(res.body.length, 1);
assert.equal(res.body[0].title, "學 Node");
});
test("POST /api/todos 應該建立待辦", async () => {
const res = await request(app)
.post("/api/todos")
.send({ title: "買牛奶" });
assert.equal(res.status, 201);
assert.equal(res.body.title, "買牛奶");
});
分工總結#
| 測試類型 | 工具 | 測什麼 |
|---|---|---|
| 單元測試 + Mock | node:test + mock.fn | 函式邏輯(假的依賴) |
| API 測試 | supertest | 路由、狀態碼、回應格式 |
| E2E 測試 | Playwright | 完整使用者流程(見 5.1 金字塔) |
原則:單元測試用 mock 隔離外部世界;API 測試驗證整個 HTTP 層;兩者都用假的資料層,又快又穩定。