---
title: "測試全綠但庫存超賣？用 Bruno 實現「程式生成」的電商 API 整合測試"
description: "API 一支一支測都是綠燈，客戶卻被扣了兩次錢。這篇用電商場景實跑一套 Bruno 整合測試，教你怎麼用「對帳」的思維取代逐個欄位比對， 用 Node.js 腳本直接查資料庫，還能用程式一次生出上百支測試。全文附真實執行截圖。"
canonical_url: "https://blog.markkulab.net/post/bruno-generated-ecommerce-api-integration-testing"
author: "Mark Ku"
author_url: "https://blog.markkulab.net/author/mark-ku"
site: "Mark Ku's Blog"
date_published: "2026-08-07 20:18:37 +0800"
category: "Tech Sharing"
language: "zh-TW"
license: "CC BY 4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
attribution: "轉載或引用請註明作者並附上原文連結"
---

# 測試全綠但庫存超賣？用 Bruno 實現「程式生成」的電商 API 整合測試

## 1. 前言：測試全部綠燈，為什麼客戶還是被扣兩次錢？

先講一個大家都遇過的場景。

所有 API 測試都回 `200 OK`，欄位格式一個不差，CI 上的報告一片漂亮的綠。結果客服那邊開始收到抱怨：使用者因為網路卡頓連點了兩次，購物金被扣了兩次；或是明明已經沒貨的商品，竟然被買走了。

問題出在哪？**一支 API 測試只能證明「這一次呼叫回得對」，但真實的業務是一連串動作串起來的。**

所以最可怕的 Bug 從來不是某支 API 回 500，那種一眼就看得到。可怕的是每支 API 都乖乖回 200，但使用者的錢和系統的庫存就是對不起來。

先前那篇 [告別 Postman 收費陷阱！開源 Git 原生 API 測試神器 Bruno 實戰指南](https://blog.markkulab.net/post/bruno-postman-alternative-guide) 聊的是工具選擇與基本設定。這篇要往下一層走：怎麼從「測單支 API」升級成「測整串流程」，而且讓這件事可以規模化。

## 2. AI 時代真正卡住的地方：不是寫，是驗

這一年最大的體感變化是，**寫程式已經不是瓶頸了**。需求進來，AI 半小時內就能把一支 API、一個頁面寫到七八成。

但是「把這些東西驗過一遍」的速度，完全沒有跟著變快。於是瓶頸整個往後移了。

寫得越快，要驗的組合越多。而人工點畫面、手動開資料庫比對，成本是線性成長的，追不上 AI 的產出速度。真正該投資的，是一套**可以重複跑、可以用程式生出來、可以塞進 CI 自動執行**的驗證流程。

我找了一輪工具，最後停在 Bruno，理由很單純：

| 我需要什麼 | Bruno 給了什麼 |
| :--- | :--- |
| 讓 AI 好改 | `.bru` 就是純文字檔，AI 讀得懂、改得動、可以一次生一百支。Postman 那包巨大 JSON 做不到 |
| 能查資料庫 | 測試腳本本身就是 Node.js，把 DB 連線套件 `require` 進來就能下 SQL |
| 能接 CI | 有官方 CLI [2]，跑完可以輸出 HTML / JSON / JUnit 報告，也有官方 Docker image [3]，GitLab CI 掛上去就能跑 |
| 好版控 | 檔案就是測試，`git diff` 看得懂，不用雲端帳號也不用買席次 |

整套方法論其實只有三個動作，而且順序不能顛倒：

```mermaid
---
title: 整合測試的三段式驗證
---
flowchart LR
  A["呼叫 API<br/>把流程跑一遍"] --> B["檢查回傳結果<br/>它說它成功了嗎"]
  B --> C["去資料庫確認<br/>它是真的有做嗎"]
```

第一關擋掉壞掉的 API，第二關擋掉回錯資料的 API，第三關擋的是最陰險的那種：**API 回 200、欄位也對，但資料庫裡的帳是歪的**。文章後面會用一個實際跑出來的例子證明，只有第三關擋得住它。

> 這篇的截圖都不是示意圖。我另外寫了一支 Node.js + SQLite 的假電商 API 當靶場，下面每張圖都是這套測試真的打上去跑出來的畫面。

## 3. 核心觀念：不要比對欄位，要「對帳」

如果整合測試只是把每支 API 的回傳欄位逐一檢查一遍，很容易見樹不見林。真正該驗的，是系統的**不變式**，講白話一點就是「怎麼算都應該成立的那條算式」[6]。

### 像對帳一樣測試

想像你出門買東西：

1. **出門前先數錢包**，記下有多少錢（我們叫它 B0，也就是起始值）。
2. **出去買一輪**，下單、付款、退一件、再取消一張。
3. **回家再數一次錢包**，看看少掉的金額，是不是剛好等於你應該花的錢。

寫成測試就長這樣：

```mermaid
---
title: 像對帳一樣測試：比的是「差多少」，不是「剩多少」
---
flowchart TD
  A["① 出門前先數錢包<br/>讀起始值 B0"] --> B["② 出去買一輪<br/>下單 / 付款 / 退貨 / 取消"]
  B -->|系統實際做了什麼| C["③ 回家再數一次錢包<br/>讀結束值 B1"]
  B -->|帳面上應該扣多少| E["預期變動 Δ<br/>每個動作的金額加總"]
  C --> D{"B0 - B1 等於 Δ 嗎?"}
  E --> D
  D -->|是| G["綠燈<br/>這一輪的帳對得起來"]
  D -->|否| R["紅燈<br/>重複扣款 / 少扣 / 該退的沒退"]
```

### 為什麼要記「差多少」，不記「剩多少」

寫斷言的時候，很直覺會寫成 `balance == 1000`。但只要測試環境是共用的，別人動一筆資料，你的測試就紅了。

改成記錄差額，例如 `balance == B0 - 600`，不管起始餘額是 1000 還是 5000 都會過。這樣測試就不怕別人污染資料，也不怕換順序執行。

### 三條實務規則

1. **每個情境自己讀自己的起始值**：不要共用全域變數，每個測試資料夾一開始都自己呼叫 API 讀一次，情境之間才不會互相干擾。
2. **小數點先四捨五入再比**：電商到處都是折扣和稅率，兩邊都先跑一次 `Math.round(x * 100) / 100`，才不會因為 JavaScript 的浮點數誤差莫名其妙變紅燈。
3. **把預期數字寫進斷言名稱**：例如取名 `Verify balance (expected 850)`。這樣測試掛掉時，看報告就知道差多少，不用回頭翻 log。

## 4. Bruno 的三個關鍵功能

要跑上面這套流程，工具得能傳遞狀態、能組織結構。開源的 Bruno [1] 這兩件事都做得不錯。

### 上一步的結果，傳給下一步

整合測試本質上就是一串接力（Request Chaining）[5]。Bruno 用 `bru.setVar` 和 `bru.getVar` 在請求之間傳值。

第一步讀完餘額，先存起來：

```javascript
// 讀取初始餘額並存成變數
bru.setVar('s01_bal0', Number(res.getBody().member.balance));
```

最後一步驗證時，先算出「應該剩多少」，再交給宣告式的 `assert` 去比：

```javascript
// 在 pre-request 算出期望值
const b0 = Number(bru.getVar('s01_bal0'));
bru.setVar('s01_expBal', Math.round((b0 - 600) * 100) / 100);
```

```
// 在 assert 區塊比對
assert {
  res.status: eq 200
  res.body.member.balance: eq {{s01_expBal}}
}
```

複雜的計算用程式寫，最後的比對用宣告式寫。報告乾淨，彈性也留著。

### 整包共用的開頭腳本

有些事情每支請求都要做，例如登入拿 token。與其每支都寫一遍，不如拉到 Collection（整個集合）的最上層做一次：

* **Token 只算一次**：後面所有請求直接重用，不用每次都跑登入流程。
* **防呆閘門**：整合測試會真的改到資料，所以絕對不能誤打正式站。在集合最上層加這幾行擋住：

```javascript
// Collection 層級的 script:pre-request
const url = (bru.getEnvVar('baseUrl') || '').toLowerCase();
if (!url) {
  throw new Error('No baseUrl - select the UAT environment first.');
}
if (/prod|production|live/.test(url)) {
  throw new Error('BLOCKED: baseUrl looks like production. UAT only!');
}
```

這道閘門不是寫爽的，它真的會擋。下面是我把網址硬改成一個看起來像正式站的位址之後的結果，第一支請求就被打斷，一筆資料都沒進去：

![Bruno collection 層級安全閘門攔截 production baseUrl 的 CLI 輸出](https://blog.markkulab.net/content/markku/posts/bruno-generated-ecommerce-api-integration-testing/images/bruno-production-safety-gate.webp)

再加上「乾脆不要建立正式站的環境設定檔」這條規矩，就很難出事了。

### 資料夾就是測試結構

Bruno 的 `.bru` 檔裡有個 `seq` 決定執行順序，資料夾則天然就是情境分組。一個業務情境放一個資料夾：

```
requests/
├─ 00-launch/            # 先建立 Session，後面都要用
├─ 02-shop-a/            # 平台 A
│   ├─ 01-regular-checkout/  # 正常買一次
│   │   ├─ 1-read-stock.bru  # 記下庫存起始值
│   │   ├─ 2-read-balance.bru# 記下餘額起始值
│   │   ├─ 3-create-order.bru# 下單
│   │   ├─ 4-pay.bru         # 付款
│   │   ├─ 5-ship.bru        # 出貨
│   │   ├─ 6-verify-stock.bru# 核對庫存
│   │   └─ 7-verify-ledger.bru # 核對餘額 + 資料庫
│   ├─ 02-duplicate-payment/ # 重複付款
│   └─ 03-out-of-order/      # 順序顛倒
└─ 03-shop-b/            # 平台 B，一樣六個情境，規則不同
```

跑整套的時候 Bruno 會照順序走完，報告的結構就跟業務情境長得一模一樣，很好讀。

## 5. 六種「最容易出事」的情境

寫整合測試不要照著 API 規格書一支一支敲，要照著「系統會怎麼壞」來設計。電商系統除了正常購買，真正會出事的是連點、網路延遲、以及各種被拒絕的請求。

參考業界做法（例如 Stripe 的冪等性設計 [4]），我整理成六個情境：

| 分類 | 測什麼 | 在防哪一種真實災情 |
| :--- | :--- | :--- |
| **正常流程** | 下單 → 付款 → 出貨 | 基本功能壞掉，或庫存、金額算錯 |
| **重複送出** | 同一筆付款（帶同一組收據編號）送兩次 [4] | 使用者連點、Webhook 重試造成的**重複扣款** |
| **順序顛倒** | 還沒收到付款成功，先收到出貨請求 | 網路延遲讓狀態機錯亂，沒付錢就出貨 |
| **金額變動** | 取消訂單、部分退款、運費與折價券 | 退款金額算錯，運費該不該退各說各話 |
| **被拒絕（庫存）** | 只剩 2 件卻下訂 7 件 | 超賣，以及「被拒絕的請求偷偷改了資料」 |
| **被拒絕（餘額）** | 餘額只剩 120 卻要買千元商品 | 餘額變負數，或被拒絕後還是寫了一筆帳 |

這裡插一個關於「冪等性」的白話解釋：它講的是**同一個動作做幾次，結果都應該一樣**。就像電梯按鈕按十下，電梯還是只來一次。付款 API 的做法是讓前端帶一組「收據編號」（Idempotency Key），伺服器看到重複的編號，就把第一次的結果原封不動再回一次，而不是再扣一次錢。

六個情境乘上兩組平台設定，就是這套 demo 的全部。跑起來長這樣：

![Bruno CLI 跑完 55 支 request、85 個 assertion 全綠的執行結果](https://blog.markkulab.net/content/markku/posts/bruno-generated-ecommerce-api-integration-testing/images/bruno-cli-full-run-green.webp)

55 支請求、55 個 test、85 個 assert，不到 0.4 秒跑完。重點不是快，是**這 140 個檢查點每次 commit 都可以無痛重跑**。這就是「系統化驗證」跟「人工點一點」的差別。

## 6. 別在測試裡寫死數字

整合測試最容易爛掉的地方，是滿天飛的 Magic Number：

```javascript
// 三個月後沒有人知道 600 是怎麼算出來的
expect(res.getBody().member.balance).to.equal(b0 - 600);
```

只要換一家合作電商、改一次運費規則，這些數字就全錯，而且錯得無聲無息。

解法是把「這家平台怎麼算」抽出來變成一張**規則表**，讓期望值由程式推導：

```json
{
  "shop-a": {
    "cancelSemantics": "refund-all",
    "insufficientStockBehavior": "block-order",
    "shippingFee": 60,
    "taxRate": 0
  },
  "shop-b": {
    "cancelSemantics": "keep-shipping-fee",
    "insufficientStockBehavior": "block-order",
    "shippingFee": 80,
    "taxRate": 0.05
  }
}
```

翻成白話就是：A 店取消訂單全額退，運費 60，不含稅；B 店取消訂單運費不退，運費 80，另外加 5% 稅。

接著把算法寫成單純的函式，整包測試共用：

```javascript
// lib/expectations.js
const round2 = (n) => Math.round(n * 100) / 100;

// 訂單金額 = 商品金額 x (1 + 稅率) + 運費 - 折價
function orderAmount(profile, unitPrice, qty, coupon) {
  const goods = round2(unitPrice * qty);
  return round2(goods * (1 + profile.taxRate) + profile.shippingFee - (coupon || 0));
}

// 取消訂單退多少，看這家平台的規則
function cancelRefund(profile, amount) {
  return profile.cancelSemantics === 'keep-shipping-fee'
    ? round2(amount - profile.shippingFee)
    : round2(amount);
}

module.exports = { round2, orderAmount, cancelRefund };
```

測試裡就不再有硬寫死的數字，只剩「按照這家的規則，答案應該是多少」：

```javascript
// script:pre-request
const P = { cancelSemantics: 'refund-all', shippingFee: 60, taxRate: 0 };
const exp = require(bru.cwd() + '/lib/expectations.js');
bru.setVar('s01_expAmount', exp.orderAmount(P, Number(bru.getVar('s01_price')), 2, 100));
```

好處有兩個。第一，換平台只要換那張規則表，測試一行都不用改。第二，推導出來的數字會直接出現在報告的斷言名稱上，例如 `Refund follows profile.cancelSemantics=keep-shipping-fee (expected 189)`。紅燈時一眼就知道是我對規則理解錯了，還是系統真的算錯。

## 7. 測試不用一支一支手刻

到這裡，整套測試已經很規律了：六個情境、固定的三段式驗證、期望值由規則表推導。既然這麼規律，就沒有理由用手刻。

Postman 的 Collection 是一整包巨大的 JSON，程式生成它不是不行，但生出來沒人看得懂 diff。Bruno 的 `.bru` 是純文字，用字串模板就能吐：

```javascript
// generate.mjs（節錄）
function req({ name, seq, method, url, body, assert = [], tests }) {
  const parts = []
  parts.push(`meta {\n  name: ${name}\n  type: http\n  seq: ${seq}\n}`)
  parts.push(`${method.toLowerCase()} {\n  url: ${url}\n  body: ${body ? 'json' : 'none'}\n  auth: inherit\n}`)
  if (body) parts.push(`body:json {\n${indent(body)}\n}`)
  if (assert.length) parts.push(`assert {\n${assert.map((a) => `  ${a}`).join('\n')}\n}`)
  if (tests) parts.push(`tests {\n${indent(tests)}\n}`)
  return parts.join('\n\n') + '\n'
}
```

於是多接一家合作電商的成本，就從「重刻一整套測試」變成「填一張規則表」：

![測試生成器讀取兩組 profile 後產生 12 個情境資料夾、55 支 .bru 檔的輸出](https://blog.markkulab.net/content/markku/posts/bruno-generated-ecommerce-api-integration-testing/images/bruno-generator-output.webp)

這是生成器真的跑一次的畫面：兩張規則表進去，12 個情境資料夾、55 支 `.bru`、75 個檔案出來，不到一秒。

程式生成帶來的好處是會累積的：

* **多接一家平台幾乎不花錢**：覆蓋率立刻補齊，不用人重寫一遍。
* **格式完全一致**：所有平台的資料夾結構、變數命名、斷言措辭都一樣，報告可以並排比對。
* **方法論可以整批升級**：想讓每個情境都多檢查一條「訂單最後必須是 SHIPPED」？改生成器一行，重跑，55 支測試同步更新。

這也是我說 Bruno「AI 友善」的真正意思。純文字讓 AI 和腳本都能安全地大量改測試，而測試本身是可執行的，改壞了立刻紅給你看。

## 8. API 說成功，資料庫真的有記到嗎？

到目前為止，我們驗的都還是「API 說的話」。

但是餘額查詢 API 回得正確，不代表資料庫裡的**交易明細**有被正確寫入。少寫一筆對帳紀錄、多寫一筆重複紀錄，在 API 那一層完全看不出來。

這就是 Bruno 用 Node.js 當腳本引擎最值錢的地方：測試可以直接連資料庫。

```javascript
// lib/ledger.js
const { createRequire } = require('module');

function openDb(collectionPath, dbRelPath) {
  const nodeRequire = createRequire(collectionPath + '/bruno.json');
  const { DatabaseSync } = nodeRequire('node:sqlite');
  return new DatabaseSync(collectionPath + '/../' + dbRelPath, { readOnly: true });
}
```

這邊示範用 SQLite，實際專案換成 `mssql`、`mysql2` 或 `pg` 寫法完全一樣，差別只在連線字串。

接著定義四條資料庫層級的檢查，我把它們編號成 C1 到 C4：

| 代號 | 白話說明 | 在抓什麼 |
| :--- | :--- | :--- |
| **C1 有扣就要有紀錄** | 每一筆成功扣款，明細裡都要找得到 | 錢扣了卻沒留紀錄，事後對不了帳 |
| **C2 不能憑空多出來** | 明細裡的每一筆，都要追得回測試建立的訂單 | 系統偷偷寫入來路不明的交易 |
| **C3 同一件事只能記一次** | 同一張訂單的同一個動作，最多一筆 | 資料庫層級的冪等性破功 |
| **C4 加總要對得起來** | 明細金額加總，要等於餘額實際少掉的錢 | **API 跟帳本各說各話** |

把它們掛在每個情境的最後一支請求上，一支請求就同時做完三件事：呼叫 API、檢查回傳、確認資料庫。

![Bruno 一支 request 內完成 API 斷言與 C1 至 C4 資料庫帳本驗證的執行結果](https://blog.markkulab.net/content/markku/posts/bruno-generated-ecommerce-api-integration-testing/images/bruno-db-ledger-invariants.webp)

### C4 才是真正的守門員

前面三條看起來都很合理，但它們其實都還在問「系統有沒有自打嘴巴」。真正致命的是 C4，因為它把兩個獨立的事實綁在一起比對：**明細的加總，必須等於餘額實際少掉的金額**。

我故意把假伺服器的冪等性拿掉，模擬一次真實的 regression（有人改了付款流程，忘記檢查收據編號），然後重跑「重複付款」情境：

![關閉冪等性後重跑重複付款情境，API 回 200 但帳本淨額與餘額變動不符的紅燈輸出](https://blog.markkulab.net/content/markku/posts/bruno-generated-ecommerce-api-integration-testing/images/bruno-double-charge-regression.webp)

注意這張圖的細節，開頭那個客服災情在這裡完整重現：

* 兩次付款 API **都回 200**，回傳的金額也都正確。
* 資料庫的唯一索引擋掉了第二筆明細，所以 **C3「只能記一次」還是綠的**。
* 但會員餘額被扣了兩次：`AssertionError: expected -380 to equal -760`。明細說這張單只動了 380，實際錢包卻少了 760。

如果測試只做到「檢查 API 回傳」，這個 Bug 會全綠上線。**只有把資料庫拉進來對帳，它才會在 CI 就被攔下來。**

## 9. 密鑰怎麼放，怎麼接進 CI

整合測試會碰到真實密鑰，所以在接 CI 之前，先把規矩定死。

### 密鑰從哪裡讀，優先順序

1. **系統環境變數**（`bru.getProcessEnv('SHOP_API_KEY')`）：CI 用這個，由 GitLab 的 CI/CD Variables（Masked + Protected）注入。
2. **專案根目錄的 `.env`**：本機開發用。
3. **Bruno 環境檔**：只放不敏感的預設值，例如 `baseUrl`。

```javascript
// Collection 層級：CI 優先吃環境變數，本機才 fallback
bru.setVar('apiKey', bru.getProcessEnv('SHOP_API_KEY') || bru.getEnvVar('apiKey'));
```

### 三條不能破的規矩

* 密鑰不進 Git，repo 裡只留 `.env.example`。
* 密鑰不放命令列參數，不然會出現在 process 清單和 CI log 裡。
* 測試報告（HTML / JSON）裡面有 request 和 response 內容，一律進 `.gitignore`，或用 `--reporter-skip-headers` 之類的參數過濾掉。

### 接上 GitLab CI

`SHOP_API_KEY` 在 GitLab 專案的 Settings → CI/CD → Variables 建一個 Masked 變數就好，Runner 會自動注入成環境變數，`.gitlab-ci.yml` 裡不用再寫一次：

```yaml
stages:
  - test

api-integration:
  stage: test
  image: node:22
  script:
    - cd collection
    - >
      npx @usebruno/cli run requests -r
      --env uat --sandbox developer
      --reporter-html ../report.html
      --reporter-junit ../junit.xml
  artifacts:
    when: always
    expire_in: 1 week
    paths:
      - report.html
      - junit.xml
    reports:
      junit: junit.xml
```

`artifacts:reports:junit` 讓 GitLab 把失敗案例直接標在 Merge Request 的 Test summary 上，`--reporter-html` 則產出這份可以丟給非工程角色看的報告（放在 `paths` 裡，Job 頁面可以直接下載）：

![Bruno HTML 報告 dashboard，顯示 55 個 request、140 個檢查點全數通過](https://blog.markkulab.net/content/markku/posts/bruno-generated-ecommerce-api-integration-testing/images/bruno-html-report.webp)

有兩個地方容易忘記。第一，`when: always` 一定要加，不然測試紅燈時 Job 直接失敗，報告根本不會被收走，等於最需要看的時候看不到。第二，Bruno CLI 測試失敗時回非 0 的 exit code，Job 就會是紅的，這是我們要的行為；如果只想先觀察一陣子不擋合併，加 `allow_failure: true` 而不是把指令用 `|| true` 吞掉。

官方另外也有 Docker image [3]，不想在 CI 處理 Node 版本的話，把 `image:` 換成官方 image 就能跑。

## 10. 我們踩過的雷

| # | 踩到什麼 | 怎麼解 |
| :--- | :--- | :--- |
| 1 | 規則用猜的。文件寫「取消全額退」，實際會扣運費 | 每個規則都用一次真實呼叫實測確認，不要相信文件 |
| 2 | 從錯誤回應讀狀態。庫存不足時拿 error body 裡的數字去對帳 | 錯誤回應只檢查 status 和錯誤碼，狀態一律重新 GET 一次 |
| 3 | 看起來成功但其實沒做事。重複付款回 200，卻沒真的扣款 | 靠最後的餘額與明細加總去抓，不要相信中間步驟的 200 |
| 4 | 17 位數的訂單編號被 `JSON.parse` 吃掉精度 | 編號一律 `String()` 之後再傳，檢查長度而不是數值 |
| 5 | 只跑子資料夾，因為少了 `00-launch` 沒建立 session 而全滅 | 執行時把 `00-launch` 一起帶上，或在集合層級做 token 快取 |
| 6 | 浮點數：`567 * 1.05` 兩邊算出來差一點點 | 兩邊都先 `Math.round(x * 100) / 100` 再比 |
| 7 | PowerShell 5.1 跑腳本中文變亂碼 | `.ps1` 只寫英文，中文說明放 Markdown |
| 8 | Bruno 的 sandbox 載不進 `node:sqlite` | 用 `require('module').createRequire(...)` 繞過，或改用一般的 npm 套件 |

第 8 條是這次現踩的。Bruno 判斷「這是不是 Node 內建模組」時，會把 `node:` 前綴剝掉再去查清單，但 `node:sqlite` 在清單裡剛好是帶前綴登錄的，於是被誤判成 npm 套件，直接噴找不到檔案。`createRequire` 一行就解掉。

這也提醒一件事：**沙盒能做什麼不能做什麼，要用跑的去確認，不要用猜的**。

## 11. 結語：防禦可以加速，但邊界還是要人來定義

回到開頭那個問題。這套東西真正改變的不是「測試寫得比較快」，而是**驗證從一次性的動作，變成一道可以重複執行的防線**。

| 項目 | 這套 demo 的實測數字 |
| :--- | :--- |
| 平台規則表 | 2 張 |
| 情境資料夾 | 12 個（6 情境 × 2 平台） |
| 生出來的 `.bru` 請求 | 55 支 |
| 檢查點（test + assert） | 140 個 |
| 全套執行時間 | 0.4 秒 |
| 多接一個平台的成本 | 一張規則表 |

但有件事要講清楚：**防禦可以加速，邊界不行**。

生成器可以幫你把 55 支測試變成 550 支，AI 可以幫你把斷言寫得又快又漂亮，CI 可以每次 push 都跑一遍。

但是「這家平台取消訂單到底該不該退運費」、「重複付款算不算錯」、「帳對不起來要不要擋上線」，這些**邊界**機器沒辦法幫你決定。你定義錯了，它只會很有效率地把錯的規則驗證一千次。

所以工具會換，Bruno 也不會是終點。真正留得住的是那套方法論：用對帳取代欄位比對、照著故障模式設計情境、讓期望值由規則推導出來，最後一定要**打完 API 就回頭問資料庫一次**。

這三個動作的順序，就是這篇文章想留下的東西。

```mermaid
flowchart LR
  A["呼叫 API"] --> B["檢查回傳結果"] --> C["去資料庫確認"]
```

## 參考資料

1. [Bruno Official Site](https://www.usebruno.com/)
2. [Bruno CLI](https://blog.usebruno.com/bruno-cli)
3. [Official Bruno Docker Image and GitHub Action](https://blog.usebruno.com/official-bruno-docker-image-and-github-action)
4. [Stripe: Designing robust and predictable APIs with idempotency](https://stripe.com/blog/idempotency)
5. [Request Chaining - Bruno Docs](https://docs.usebruno.com/v2/testing/script/request-chaining)
6. [Invariant Test - Cyfrin Glossary](https://www.cyfrin.io/glossary/invariant-test)

---

## 關於本文與作者

本文出自 [Mark Ku's Blog](https://blog.markkulab.net/post/bruno-generated-ecommerce-api-integration-testing)

授權條款： [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) — 轉載或引用請註明作者並附上原文連結

### 關於作者

**[Mark Ku](https://blog.markkulab.net/author/mark-ku)** — Software Solution Provider

- 10+ 年資深軟體工程師，現為 AI 應用 Builder
- 專注大型平台架構設計，從北美電商到AI SaaS訂閱收費系統
- 結合 AI Agent 與自動化，打造高效可演進的產品技術基礎

### 作者開發的免費工具

以下工具皆可免費使用：

- [免費 PDF 簽名工具](https://blog.markkulab.net/tools/pdf-sign): 線上 PDF 簽名工具，瀏覽器內完成手繪、打字、上傳簽名，可拖曳放置、縮放、下載。所有處理都在你的裝置完成，檔案不會上傳。
- [VS Code Refactory](https://blog.markkulab.net/tools/refactory): Refactory 是一款 VS Code 重構擴充套件：34 個重構動作、37 條 code smell 檢查、Code Health 儀表板、18 種語言、534 支測試。懂你的專案慣例：介面放哪、DI 註冊寫在哪、'use client' 該不該加；還會用 git 修改頻率 × 複雜度排出「該先修哪個檔案」，並一鍵把壞味道交給你自己電腦上的 Claude Code 修。免費使用，原始碼不離開你的機器。
- [DB-Kit 資料庫管理工具](https://blog.markkulab.net/tools/db-kit): DB-Kit 是一個用 Tauri + Rust + React 打造的輕量跨平台資料庫管理工具，用單一一致的介面同時管理 MySQL、MariaDB、PostgreSQL、SQL Server、Oracle、SQLite、MongoDB、Redis、Kafka、Elasticsearch 與 RabbitMQ 十一種資料來源：連線密碼以 OS keychain 加密、SSH Tunnel、完整 CRUD、視覺化查詢建構器、多結果集同時顯示、跨連線資料傳輸與比對同步、Excel / CSV 匯入匯出、執行計畫視覺化、ER 圖、排程備份、SQL 壓力測試（p50～p99 延遲百分位）、15 條規則的 SQL 審查、Kafka 訊息瀏覽與監控告警；繁中 / 英文雙語介面，內建 AI 助手（自然語言生成 SQL、AI 審查與調校建議）與命令列工具 dbk。免費開源（MIT），提供 Windows / macOS / Linux 安裝檔。
- [VS Code Super Mermaid](https://blog.markkulab.net/tools/super-mermaid): Super Mermaid 是一款 VS Code 擴充套件：開箱即用的漂亮 Mermaid 圖表，自動上色、即時預覽、滑鼠平移縮放、PNG / SVG 高解析匯出，內建 21 種範本與多種主題。免費開源（MIT）。
- [React Super Mermaid](https://blog.markkulab.net/tools/react-super-mermaid): react-super-mermaid 是一個開源 React 元件庫：一行 <MermaidViewer> 即可渲染漂亮的 Mermaid 圖表，內建 colorful / sketch 主題、平移縮放、圖內搜尋、SVG / PNG 高解析匯出。輕量、SSR 安全、完整 TypeScript 型別。免費開源（MIT）。
- [Jira / Confluence Super Mermaid](https://blog.markkulab.net/tools/jira-super-mermaid): Atlassian Forge app：在 Jira issue 與 Confluence 內文直接寫 Mermaid 語法，畫流程圖、時序圖、狀態機與甘特圖。11 種圖表、SVG / PNG 匯出、明暗主題、完整中日韓文字支援。取得 Runs on Atlassian 資格：圖表存在你自己的站台，app 不呼叫任何第三方服務。免費，即將上架 Atlassian Marketplace。
- [Mermaid 線上預覽](https://blog.markkulab.net/tools/mermaid-preview): 在瀏覽器裡寫 Mermaid、即時看圖，整張圖表壓進網址就能分享。免註冊、不上傳伺服器，相容 mermaid.live 的分享連結。
- [React Intl Phone Number](https://blog.markkulab.net/tools/react-intl-phone-number): react-intl-phone-number 是一個開源 React 元件：framework-agnostic、不依賴 antd，提供 E.164 進出、可搜尋國旗 / 國碼下拉、可配置驗證等級（strict / mobile-strict / loose）、可主題化 CSS 與 i18n，電話邏輯由 google-libphonenumber 驅動。輕量、完整 TypeScript 型別。免費開源（MIT）。
- [Uptime Kuma Cluster](https://blog.markkulab.net/tools/uptime-kuma-cluster): 把單機版 Uptime Kuma 改造成高可用叢集：OpenResty + Lua 智慧負載平衡、MariaDB 共享狀態、健康檢查與自動 Failover，附叢集管理 REST API，一行 Docker Compose 啟動。免費開源（MIT）。
- [特教專案](https://blog.markkulab.net/education): 為特殊教育學生製作的學習教材

### 每日 Podcast

- [Mark's Tech Insights 每日 AI 新鮮事](https://blog.markkulab.net/category/tech-news): 每日精選 AI 與科技趨勢，透過語音摘要快速掌握最新技術動態，涵蓋 AI 應用、軟體架構、DevOps 與工程實戰。 — RSS: https://blog.markkulab.net/feed.xml
- [AI股市蝦聊](https://blog.markkulab.net/category/ai-stock-chat): 每個交易日用 AI 分析台股盤勢，以雙人對話聊當天的盤中觀察與隔日預測。 — RSS: https://blog.markkulab.net/ai-stock-chat/feed.xml
- [開源好物週報](https://blog.markkulab.net/category/open-source-weekly): 每週從 Hacker News、GitHub、Reddit 聲量雷達精選免費開源工具，以雙人對話聊解決什麼痛點與怎麼快速上手。 — RSS: https://blog.markkulab.net/open-source-weekly/feed.xml

### 電子報

[訂閱電子報](https://blog.markkulab.net/subscribe) — 第一時間收到新文章通知，無垃圾信、隨時可取消訂閱。
