API 測試實戰:Postman、MSW 與 Vitest
API 測試是確保服務品質的關鍵。本篇將介紹三種主流的 API 測試工具與方法。
一、 Postman
1.1 基本使用
javascript
// 發送請求
POST https://api.example.com/users
Content-Type: application/json
{
"name": "John",
"email": "john@example.com"
}1.2 環境變數
javascript
// 定義環境
{
"baseUrl": "https://api.example.com",
"token": "your-token"
}
// 使用變數
GET {{baseUrl}}/users
Authorization: Bearer {{token}}1.3 測試腳本
javascript
// Tests 標籤頁
pm.test("Status code is 200", () => {
pm.response.to.have.status(200);
});
pm.test("Response has users", () => {
const json = pm.response.json();
pm.expect(json.data).to.be.an("array");
pm.expect(json.data.length).to.be.greaterThan(0);
});
pm.test("User has required fields", () => {
const user = pm.response.json().data[0];
pm.expect(user).to.have.property("id");
pm.expect(user).to.have.property("name");
pm.expect(user).to.have.property("email");
});
// 設置變數供後續請求使用
pm.test("Save user id", () => {
const user = pm.response.json().data[0];
pm.environment.set("userId", user.id);
});1.4 Pre-request 腳本
javascript
// Pre-request Script 標籤頁
// 生成動態資料
pm.environment.set("timestamp", Date.now());
pm.environment.set("randomEmail", `user${Date.now()}@test.com`);
// 生成簽名
const crypto = require("crypto-js");
const signature = crypto
.HmacSHA256(pm.request.body.raw, pm.environment.get("secretKey"))
.toString();
pm.request.headers.add({ key: "X-Signature", value: signature });1.5 Collection Runner
javascript
// 批次執行測試
// 1. 選擇 Collection
// 2. 設定迭代次數
// 3. 選擇資料檔案 (CSV/JSON)
// data.json
[
{ name: "User 1", email: "user1@test.com" },
{ name: "User 2", email: "user2@test.com" },
];1.6 Newman CLI
bash
# 安裝
npm install -g newman
# 執行集合
newman run collection.json -e environment.json
# 生成報告
newman run collection.json \
-e environment.json \
-r htmlextra \
--reporter-htmlextra-export report.html二、 MSW(Mock Service Worker)
MSW 讓你在瀏覽器和 Node.js 中 Mock API。
2.1 安裝設置
bash
npm install msw --save-dev2.2 定義 Handlers
javascript
// mocks/handlers.js
import { http, HttpResponse } from "msw";
export const handlers = [
// GET 請求
http.get("/api/users", () => {
return HttpResponse.json({
data: [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" },
],
});
}),
// POST 請求
http.post("/api/users", async ({ request }) => {
const body = await request.json();
return HttpResponse.json({ id: 3, ...body }, { status: 201 });
}),
// 動態路由
http.get("/api/users/:id", ({ params }) => {
const user = users.find((u) => u.id === params.id);
if (!user) {
return HttpResponse.json({ error: "Not found" }, { status: 404 });
}
return HttpResponse.json(user);
}),
// 延遲回應
http.get("/api/slow", async () => {
await delay(2000);
return HttpResponse.json({ message: "Slow response" });
}),
// 錯誤回應
http.get("/api/error", () => {
return HttpResponse.json({ error: "Server error" }, { status: 500 });
}),
];
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}2.3 Node.js 測試整合
javascript
// mocks/server.js
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers);
// vitest.setup.js
import { beforeAll, afterEach, afterAll } from "vitest";
import { server } from "./mocks/server";
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());2.4 測試中覆蓋 Handler
javascript
import { http, HttpResponse } from "msw";
import { server } from "./mocks/server";
test("handles error", async () => {
// 臨時覆蓋
server.use(
http.get("/api/users", () => {
return HttpResponse.json(
{ error: "Service unavailable" },
{ status: 503 }
);
})
);
// 測試錯誤處理
const result = await fetchUsers();
expect(result.error).toBe("Service unavailable");
});2.5 瀏覽器整合
javascript
// mocks/browser.js
import { setupWorker } from "msw/browser";
import { handlers } from "./handlers";
export const worker = setupWorker(...handlers);
// main.js (開發環境)
if (import.meta.env.DEV) {
const { worker } = await import("./mocks/browser");
await worker.start();
}三、 Vitest API 測試
3.1 基本設置
javascript
// vitest.config.js
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "node",
setupFiles: ["./vitest.setup.js"],
},
});3.2 測試 Express 應用
javascript
// app.test.js
import { describe, it, expect } from "vitest";
import request from "supertest";
import app from "./app";
describe("Users API", () => {
describe("GET /api/users", () => {
it("should return users list", async () => {
const response = await request(app).get("/api/users").expect(200);
expect(response.body.data).toBeInstanceOf(Array);
expect(response.body.pagination).toBeDefined();
});
it("should support pagination", async () => {
const response = await request(app)
.get("/api/users")
.query({ page: 2, limit: 5 })
.expect(200);
expect(response.body.pagination.page).toBe(2);
expect(response.body.data.length).toBeLessThanOrEqual(5);
});
});
describe("POST /api/users", () => {
it("should create a user", async () => {
const userData = {
name: "Test User",
email: "test@example.com",
};
const response = await request(app)
.post("/api/users")
.send(userData)
.expect(201);
expect(response.body.name).toBe(userData.name);
expect(response.body.id).toBeDefined();
});
it("should validate email", async () => {
const response = await request(app)
.post("/api/users")
.send({ name: "Test", email: "invalid" })
.expect(422);
expect(response.body.error.code).toBe("VALIDATION_ERROR");
});
});
describe("Authentication", () => {
it("should reject without token", async () => {
await request(app).get("/api/protected").expect(401);
});
it("should accept with valid token", async () => {
const token = await getTestToken();
const response = await request(app)
.get("/api/protected")
.set("Authorization", `Bearer ${token}`)
.expect(200);
expect(response.body.user).toBeDefined();
});
});
});3.3 測試 Fixtures
javascript
// fixtures/users.js
export const testUsers = [
{ id: "1", name: "John", email: "john@test.com" },
{ id: "2", name: "Jane", email: "jane@test.com" },
];
// tests/users.test.js
import { beforeEach } from "vitest";
import { testUsers } from "./fixtures/users";
beforeEach(async () => {
await User.deleteMany({});
await User.insertMany(testUsers);
});3.4 Snapshot 測試
javascript
it("should match response structure", async () => {
const response = await request(app).get("/api/users/1").expect(200);
// 第一次執行會建立 snapshot
expect(response.body).toMatchSnapshot();
});四、 整合測試策略
4.1 測試金字塔
4.2 測試覆蓋率
javascript
// vitest.config.js
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
exclude: ['node_modules', 'tests']
}
}
})
// 執行
vitest run --coverage4.3 CI/CD 整合
yaml
# .github/workflows/test.yml
name: API Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 18
- run: npm ci
- run: npm test
- name: Newman Tests
run: |
npm install -g newman
newman run postman/collection.json \
-e postman/ci-environment.json總結
| 工具 | 用途 | 適用場景 |
|---|---|---|
| Postman | 手動 + 自動測試 | 開發除錯、團隊協作 |
| MSW | Mock API | 前端開發、隔離測試 |
| Vitest | 自動化測試 | CI/CD、回歸測試 |
> **測試策略**:
- 開發時用 Postman 探索
- 前端用 MSW 隔離
- CI 用 Vitest + Newman 自動化
進階挑戰
- 設計一套完整的 API 測試流程,從登入到 CRUD。
- 使用 MSW 實作一個可離線運作的 Web 應用。
- 研究契約測試(Contract Testing)和 Pact 工具。