跳至主要內容
Skip to content

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-dev

2.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 --coverage

4.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手動 + 自動測試開發除錯、團隊協作
MSWMock API前端開發、隔離測試
Vitest自動化測試CI/CD、回歸測試

> **測試策略**:

  • 開發時用 Postman 探索
  • 前端用 MSW 隔離
  • CI 用 Vitest + Newman 自動化

進階挑戰

  1. 設計一套完整的 API 測試流程,從登入到 CRUD。
  2. 使用 MSW 實作一個可離線運作的 Web 應用。
  3. 研究契約測試(Contract Testing)和 Pact 工具。

延伸閱讀與資源