iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
Claude AI

Claude × Playwright:從新進同事到 Agentic SDET 代理人系列 第 18

Day 18|這到底是不是 Bug?教它建立 Test Oracle

  • 分享至 

  • xImage
  •  

前言

昨天那一輪交回來的東西裡,有一筆很刺眼。

結帳地址那一步,postcode 欄位預填的值是字串 missing value。任何人看到都會皺眉:那明顯是程式內部代表「查不到值」的字面字串,怎麼會跑到使用者的表單裡,而且還通得過必填驗證。

結帳地址步驟,postcode 欄位預填字串 missing value

value="missing value"placeholder="Your Postcode *"。它是真的值,不是提示文字,不改就會被送出去。

但 finding 上寫的是 status: anomaly,不是 fail

它看到了,卻不敢說那是錯的。今天要回答為什麼,答案是判準沒寫進 charter,它就不敢判。

為什麼沒有 oracle 就沒有 bug

「這是錯的」這句話,比大多數人以為的難講。

你說 postcode 不該顯示 missing value。憑什麼?憑常識。但常識不是判準 —— 換一個產品,missing value 可能就是設計好要顯示的佔位文字;換一個團隊,那可能是他們刻意保留的除錯訊息。你覺得錯,是因為你腦子裡有一份沒說出口的期待。

Test oracle 就是那份期待被寫下來的樣子:你憑什麼說這是錯的。找不到判準,你只能說「怪」,不能說「錯」。

所以今天要把兩件事切開:

  • 發現 —— 走到那裡、看到那個現象。這是昨天的主題,靠的是啟發式。
  • 定罪 —— 說得出它違反了哪一條。這是今天的主題。

切開的好處是誠實。沒有 oracle 接得住的東西,就老實標成 anomaly 留著,不硬判成 fail。硬判的代價昨天算過了:一張退不回去的單,換來下一次沒人看你的單。

實驗:同一筆 finding,證據補上之後從 anomaly 升級成 bug

回到 postcode 那一筆。它的檔案裡有一段 verdict_history,記著它被判過兩次。

第一次判定(當天稍早):

verdict: anomaly(未判定)
oracle: user-expectation
why: "charter 四條 oracle 涵蓋不到;當時只知道 UI 顯示什麼,不知道 API 回什麼,
       分不出是後端存了這個字串還是前端捏的。缺的就是上游那一筆證據。"

關鍵在最後一句。它分不出來的不是「這對不對」,是「這是誰做的」。如果後端真的把 missing value 存進資料庫,那是資料問題;如果是前端替 null 捏出來的,那是前端問題。兩種修法完全不同,而光看畫面,這兩種長得一模一樣。

當時手上只有「使用者期待」這條最弱的 oracle 撐著,所以它標 anomaly,交出去等人定奪。

第二次判定(同一天,端點側那輪跑完之後):

verdict: bug
oracle_used: 內部一致性
basis: >
  兩處自相矛盾,都不需要外部規格:
  (1) 同一筆資料兩個值 —— GET /users/me(同一個帳號 id=2)回 postcode: null,
      但結帳地址表單的 [data-test=postcode] 顯示字串 "missing value"。
  (2) 同一張表單、同樣的上游輸入,兩種處理 —— API 對 postcode 與 state 都回 null,
      前端卻把 postcode 塞成 "missing value"、把 state 留成空字串。
      同輸入不同輸出,前後不一。
  結論:那五個字不是後端存進去的資料,是前端替 null 捏出來的字面值。

現象一模一樣,證據多了一筆,判準就從最弱那條換成最強那條。

GET /users/menull 這件事,把「後端存的」那條分支砍掉了,剩下的解釋只有一種。而一旦能指著兩個互相矛盾的值,就不再需要任何人同意「postcode 不該顯示 missing value」這件事 —— 產品自己打自己臉。

這一筆說明了今天最重要的一件事:判不動的時候,缺的通常不是判斷力,是一筆證據。標 anomaly 不是認輸,是把缺口記下來等它被補上。

要誠實講一件事:r2 那一輪的五筆 finding 裡,只有這一筆被重判過,所以只有它補上了 verdictoracle_usedbasis 三個欄位。其他四筆用的還是 oracleoracle_detail,跟 skill 規定的輸出欄位對不上。規格寫了、實作沒跟上,這種落差跟 Day 13 那個一筆都沒有的 category 是同一種病。

六條 oracle,由強到弱

Test oracle 的來源不只一種。傳統測試教科書會告訴你三類:原有系統(開發新財務系統時,拿舊系統的穩定報表當比對標準)、使用者手冊或文件(手冊說選了「兩日送達」就該顯示預計送達日)、專家知識(心電圖的判讀要靠醫生)。

這三類的共同問題是它們都要問別人。所以 test-oracle 這支 skill 把它們重排成六條,排序的依據只有一個問題:要不要外部資訊?

# oracle 判什麼 要外部資訊嗎
1 內部一致性 產品自己前後矛盾 不用
2 API 對 UI 畫面說的跟端點回的對不上 不用
3 無 console error 正常操作不該噴錯 不用
4 規格 違反寫下來的約定 要,knowledge/
5 使用者期待 違反常識與領域慣例 要,而且沒有白紙黑字
6 對照品 同一功能別的地方不是這樣 要,要有對照組

前三條最強,因為它們不用等任何人回答。你不需要找 PM 確認、不需要翻規格、不需要問「這是不是刻意設計的」。產品自己提供了矛盾,你只要指出來。

為什麼內部一致性排第一

r2 那一輪的 F-004 是最乾淨的例子:同一個畫面上,頁面內文寫著「Hello Jane Doe, you are already logged in」,導覽列同時寫著「User Data not found」。

不必知道規格、不必問任何人。這一頁自己說了兩句互相打臉的話,其中一定有一句是錯的。這種判定沒有人吵得贏,也不會因為換了 PM 就翻案。

反過來看第 5 條有多弱:postcode 第一次判定就是靠它,結果只能標 anomaly。「使用者期待」的問題不在它不對,在它沒有仲裁者 —— 你說使用者不期待看到 missing value,對方說我們的使用者不在意,這場架吵不完。

API 專屬的七條

被測物是端點的時候,前面六條照樣成立(內部一致性、對照品這些跟介面層無關),但另外還有七條只有直接打端點才用得到:

oracle 判什麼
狀態碼語意 失敗回 200 包一個錯誤訊息,這是說謊
error envelope 同一個 API 的錯誤結構要一致
冪等重放 宣稱冪等的端點,送兩次結果要一樣
分頁排序 逐頁不重疊、不漏、總數對得上
授權邊界 誰能讀誰的資料
契約 schema 回應要對得上契約,不多不少
header 與 content-type 送對的標頭、拒絕不支援的

最強的是狀態碼語意,因為它跟內部一致性同源:200 OK 配一個「付款失敗」的 body,那是產品自己打自己臉,不需要規格。

最弱的是契約 schema,因為它要有契約才可用。契約來源填「無」的時候,這條 oracle 直接停用 —— 不准拿「我覺得應該有這個欄位」當判準。

端點側判定長什麼樣,看實跑的這一筆:

id: F-001
verdict: bug
oracle_used: 授權邊界(主)+ 內部一致性(佐)
basis: >
  顧客 token(login 回 id=2、role=user)打 GET /users/1 回 200,body 含 admin 完整記錄
  (email=admin@、role=admin、password sha256 雜湊);requests.jsonl#06 /
  raw/06-users-1-idor.body.json。
  對照 #07 GET /users/999999 同 token 回 404「Requested item not found」
  → 該端點依 id 取真資料、非公開資源。
  佐證 #04:同一 token 打 GET /users(列表)被擋 401
  → 產品自己認定此 token 無權讀他人帳號,卻在 by-id 路徑放行,前後不一。

注意 basis 那三段都在做同一件事:排除別的解釋。 第二段排掉「那個端點本來就是公開的」,第三段排掉「這個 token 本來就有權限」。剩下的只有越權這一種讀法。

三種 verdict,不是兩種

判定的結果有三種,第三種最常被跳過:

verdict 意思 必須寫什麼
bug 命中某條 oracle oracle_usedbasis
needs-spec 判不動,因為沒有判準 缺哪一份規格、該問誰
inconclusive 判不動,因為證據不夠 還缺什麼證據

needs-specinconclusive 常被混在一起,但它們缺的東西不一樣:前者缺的是別人的決定,後者缺的是你自己還沒收集的資料。postcode 那一筆一開始屬於後者,所以補一輪端點證據就解決了;如果它屬於前者,跑一百輪也沒用。

inconclusive 有一個很典型的樣子。端點那一輪的 F-005 是「越權寫入提權」:讀的部分已經證實有洞,寫的部分照理說也要驗一次,但 charter 的 out_of_bounds 寫死了不准做破壞性的安全測試。

所以它停在那裡,標 inconclusive,記著等人授權。這不是能力不足,是設計。

校準:拿有標準答案的站來驗

前面講的都是自己說自己對。有沒有辦法拿外部的答案驗一次?

AcademyBugs 這個練習站給了難得的機會:站方自己植入了 25 支 bug,而且附一套確認機制 —— 你點下畫面上那個元素,它跳出兩題問答(issue type、expected result),答對才放行,然後顯示官方的 Bug #N。

2026-08-01 那一輪跑完的結果寫在 site-oracle-verdicts.yaml

result: 25/25 confirmed
final_message: "Bug #25: Congratulations! You found all 25 bugs, nice work!"
quiz_retries: 3   # bug 15 型別先答 Visual、bug 18/20 先答 Functional,各重答一次才過

25 支全中,而且這是外部 ground truth,不是自我宣告。開跑前先把 localStorage['found-bugs'] 清成 {},所以那 25 筆確定是本輪打中的。

那 3 次重答也值得記一筆:型別答錯不影響「有沒有找到」,但它說明判定分兩層 —— 找到那個現象是一回事,說得出它屬於哪一類又是一回事。

但我們交出的是 35 筆

多出來的 12 筆沒有官方答案。這一段要誠實處理,因為它才是真實情況:大多數產品沒有站方幫你確認。

那 12 筆分成兩種。

站得住的那種。 F-011 是熱門商品區塊:圖片、標題、連結各自指向三個不同的商品。這一筆不需要官方名單,內部一致性硬接得住:一個商品卡片上的三個元素指向三個東西,產品自己矛盾。

只是我方主觀的那種。 F-023 是「運費越快應該越貴」。這是常識,不是規格。站方沒說過運費怎麼算,所以它只能標 needs-spec,不能算 bug。

分野就在這裡:官方清單沒有的不等於誤報,但也不等於 bug,差別在有沒有 oracle 接得住。

四個容易判錯的地方

一、4xx 本身不是 bug。 打錯參數換來 400,那是產品做對了。要判的是「該擋沒擋」與「該過卻擋」,不是看到紅字就記一筆。

二、429 先當環境訊號。 它通常代表你打太快,不是產品壞了。先降速重試,別急著寫成缺陷。

三、契約來源填「無」的時候,schema oracle 直接停用。 沒有契約就沒有「多回了不該回的欄位」這種判定,因為你不知道什麼是該回的。

四、用了「使用者期待」這條,要在 basis 裡明講。 寫成「這是常識判斷、非規格」。因為它是六條裡最弱的一條,讀的人有權知道你的判定站在什麼上面。

怎麼讓它自己會判

test-oracle 是 model-invoked 的 skill,多數時候你不用叫它。

一、自動發生。 explorebug-hunter 走到需要判定的那一步,會自己交過來。

二、單獨重判某一筆。 補到新證據的時候用得上,postcode 那筆就是這樣升級的:

/sdet-skills:test-oracle 重判 output/sessions/2026-08-08_toolshop-checkout-payment-r2/findings/F-003-postcode-prefilled-missing-value.yaml

三、直接描述一個情況加證據。

GET /users/1 顧客的 token 回 200 帶 admin 個資,證據在
output/evidence/20260808-toolshop-api-oracles/,這算 bug 嗎

沒給證據,或者證據不足以判定的時候,它應該回 inconclusive,不該硬判。

但真正的旋鈕不在這三種用法,在 charter 的 oracles 欄位。

postcode 那筆第一次標 anomaly,根本原因是 charter 的四條 oracle 只管金額與 console:

oracles:
  - "金額內部一致:單價 × 數量 = 小計⋯"
  - "付款失敗時要有明確錯誤訊息,而且不得建立訂單"
  - "UI 顯示的結果要與 API 狀態碼一致"
  - "正常操作不應出現 console error"

四條裡沒有一條管得到「表單預填值該不該是內部字串」。想讓它敢判,判準要先寫進去。這也回到 Day 15 那句話:charter 是活的,判不動的東西會告訴你下一版該補哪一條。

小結

判定的問題不是「這對不對」,是「你憑什麼說它錯」;六條 oracle 照「要不要外部資訊」排序,前三條最強因為不必等別人回答,端點側另有七條,最強的狀態碼語意跟內部一致性同源。判不動有兩條退路:缺別人的決定寫 needs-spec,缺自己的證據寫 inconclusive。而 charter 的 oracles 欄位才是總開關 —— 沒寫進去的判準,它一條都不敢用。

                      一個現象(Day 17 挑出來的)
                                │
                                ▼
                      有沒有 oracle 接得住?
                                │
        ┌───────────────────────┼───────────────────────┐
        ▼                       ▼                       ▼
   不需外部資訊              需要外部資訊             接不住
   ①內部一致性              ④規格(knowledge/)          │
   ②API 對 UI               ⑤使用者期待                  │
   ③無 console error        ⑥對照品                     │
   (端點側另加              (端點側另加                 │
     狀態碼語意)              契約 schema)              │
        │                       │                       │
        ▼                       ▼                       ▼
     verdict: bug         判得動就 bug            needs-spec
     oracle_used          判不動往右走          (缺別人的決定)
     basis = 排除了什麼                                  或
        │                                        inconclusive
        │                                        (缺你的證據)
        │                                               │
        │                                               ▼
        │                                      補一筆證據再判一次
        │                                      postcode:anomaly → bug
        │                                      (多了 GET /users/me 回 null)
        ▼                                               │
   往下走:Day 19 的自主探索 ◄───────────────────────────┘
                                │
                                ▼
                  判不動的那些,回頭改 charter 的 oracles

找不到判準只能說「怪」,不能說「錯」,而說「錯」說得太快,代價是下一次沒人看你的單。判不動的時候缺的通常是一筆證據不是判斷力,postcode 那筆補上 GET /users/menull,判準就從最弱那條換成最強那條。而 charter 的 oracles 沒寫進去的,它一條都不敢用,所以判不動的東西會直接告訴你下一版 charter 該補什麼。

下一步

判成 bug 只是它自己說的。

明天留一個問題給這位同事:同一個 agent 又找又判,它有沒有可能只是在替自己的發現背書?

第三週的驗收是一次完整的自主探索 —— 只給 charter,中間不插手,然後看它交回來的東西。那一輪要看的就是找、判、留證三件事湊在一起,還站不站得住。


參考資料

  1. Douglas Hoffman — A Taxonomy of Test Oracles - oracle 分類的原始整理,六條表的思路來源
  2. Elisabeth Hendrickson — Explore It! - 把觀察變成判定的那一步
  3. James Bach — Exploratory Testing - Rapid Software Testing 對 oracle 的定義
  4. AcademyBugs - 25+12 筆那一節的受測站,站方自帶確認機制
  5. Toolshop(Practice Software Testing)with-bugs build - postcode 與 F-004 兩筆的受測站
  6. 本專案 skills/explore/test-oracle/ - 六條與七條的真檔
  7. 本專案 docs/explore/test-oracle.md - 設計理念
  8. 本篇證據 output/sessions/2026-08-08_toolshop-checkout-payment-r2/findings/F-003-postcode-prefilled-missing-value.yaml - 含 verdict_history
  9. 本篇證據 output/sessions/2026-08-08_toolshop-api-oracles/findings/ - 端點側五筆
  10. 本篇證據 output/sessions/2026-08-01_academybugs/site-oracle-verdicts.yaml - 25/25 逐筆對照

上一篇
Day 17|別只測 Happy Path:帶它挑戰產品假設
下一篇
Day 19|Mentor 第一次放手:完成一次自主探索任務
系列文
Claude × Playwright:從新進同事到 Agentic SDET 代理人21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言