昨天新同事提供了完整的證據,我們也看完這個證據裡面的內容,並且閱讀了新同事觀察到的現象與結論,而每個結論其實都包含著證據。我們閱讀了這份新同事的筆記,但其實我們今天無法知道哪些證據、或是哪些新同事做了哪些事情,哪些是符合我們預期的,而哪些又不是。
所以我們今天就要根據新同事提供的這個 notes.md,去看看是不是還有東西是我們要處理的。
notes.md 裡面寫了哪些...
畫面小計顯示 0
購物車列:Combination Pliers / 單價 $14.15 / 小計 $00.00,而表格底部總計是 $14.15。資料層的 total 明明是 14.15。
標題欄位重複
購物車表頭為 Item | Quantity | Price | Total | Total,最後兩欄同名。
使用者選單顯示查無使用者
登入後導覽列顯示「User Data not found」而不是姓名,但同一頁的 GET /users/me 回 200 OK。


這個問題是說,在同一張表格裡面,我們的小計是 0.0 元,但是我們在表尾顯示的其實是 14.15 元。
如果我們今天搭配一些折價券,有可能導致它的總金額是 0 元,但不可能導致它最後結算的時候是 14.15 元。所以其實這很明顯是一個 Bug。
像這種不需要額外產品規格說明的知識,其實就是一種一致性的判斷。
通常你會發現,測試案例的通過標準只有一個,就是它必須要符合你預期的行為。但當一個測試案例失敗的時候,失敗的原因可能會有很多:
因此,我們可以把失敗的測試案例分為下列幾類:
| status | 分析 |
|---|---|
| pass | 表示測試案例皆如預期的行為發生,甚至包含錯誤的情境,也會被正確擋下。 |
| fail | 表示行為不符合我們預期,它有可能是潛在的一個 bug。 |
| blocked | 前置條件可能沒有順利的設置成功,甚至沒有測到我們要測試的地方。 |
| flaky | 有時候測試案例通過,有時候失敗,它會隨機的發生。 |
| anomaly | 可能包含在這個測試案例要測試的範圍之內,它可能是我們還無法判定的可疑現象。 |
| inconclusive | 我們測到了一些異常,但是證據不足以判斷它是一個缺陷。 |
在這些類型之中,有幾種組合是比較難判斷的,例如 Block 和 Inconclusive。我舉一個例子:如果今天我們去一個購物網站,我們其實讀到產品的資訊,但畫面上整個產品的資訊整塊是沒有顯示的。同時間,我們在 API(也就是 Network 那邊)可能有看到 GET 傳回 200,證實整個畫面是有被渲染的,這樣的情況它可能就是 Inconclusive 的意思是不確定。它可能是因為網路或是某些環境造成的異常,導致我們沒辦法確定它是一個 block 的行為。
但如果今天我們在瀏覽網站的時候,看到商品頁面整個是無法撈進來的,甚至給了其他的 error(例如說 read error 403 或 404),這時候這樣的錯誤其實它就是一個 Block 的行為。
另外一個比較難判斷的,應該是 Fail 和 Anomaly。
如果今天在同一個購物車裡面的數量,它可能有兩種情況:
前端可能沒有驗證,所以導致總共的 Total 金額顯示的是負 70.75 元。這時候我們就有明確的預期行為,其實我們應該要擋下負數,所以這時候這個測試案例的失敗原因,其實它就是個 Fail。
結果它的 Total 變成 0,但你會發現到結帳的時候,它的這個金額是沒有重新被算過。那這時候我們就是要看規格上面,是不是每次在結帳之後,金額都要重算?即使它是 0。所以這樣的情況就是 Anomaly。
當我們遇到 Anomaly 的時候,其實我們可以先把它記錄下來,並不用當下就判斷它是一個缺陷,或者它是一個 Fail。
最難判斷的一個就是 flaky(不穩定的測試),要看它是穩定壞掉的,還是隨機壞掉的。
通常我們在 rerun 的時候:
通常我們在確定 flaky 的 detection 時,可能要多跑幾次,才可以知道它的 pass rate 跟 fail rate。如果它是一個很隨機的、或是 50/50 的機率,那它肯定就是一個 flaky;但如果它基本上每次都是 fail 的話,那它基本上就是一個 fail 的 test case。
最後一種是我們常常會發現的,就是整個 UI 的流程都是 pass,但其實在 console 裡會有一些紅字。
有時候紅字可能跟測試的行為沒有關係,這時候如果行為是符合的,我們可能不會管畫面上的紅字,正常來說它就是 pass。
但是,如果我們發現這個 error 可能是跟行為有關的,也許我們就要判斷它是一個 anomaly,並且告知理由是什麼。我們並不能因為畫面沒有問題,但 console 有一些 error,而忽略了一些潛在的問題。
/sdet-skills:structured-result 讀 output/evidence/20260806-with-bugs-add-to-cart/,把這一輪的檢查點寫成 results.yaml
所以今天我們要透過新的 skill structured-result,去讀我們在昨天產生的那一包證據,並且把這些測試的證據寫成 results.yaml。
這個 YAML 檔案是可以讓其他 skill 讀取的,我們會在之後的天數,再教這個系統是如何使用這個 results.yaml。
在這個封包裡面其實有兩個檔案:
notes.md:會是系統對於在探索這個網站或尋找 bug 時,所觀察到的現象與結論。manifest.md:告訴你這些產生的資料包裡面有哪些東西、放在哪裡。而我們今天寫的這個 results.yaml,主要是要給其他 skill 使用,讓後續的系統可以順利完成下一個動作;或者如果有其他需要閱讀這一次跑出來的結果,也可以去閱讀這個 results.yaml。
使用這個 skill 會產生一個 results.yaml 的 YAML 檔案。今天我們就來看看這個檔案裡面會有哪些內容。
在 content 裡面通常會列出:
results:
- id: R-01
check: "以 customer@practicesoftwaretesting.com / welcome01 登入"
status: pass
expected: "帳密不被接受時應擋下並顯示錯誤訊息"
actual: "POST /users/login 回 401,畫面顯示「Invalid email or password」,擋下的行為與 API 一致。同一組帳密在 clean 站回 200,故本站是種子資料不同(test-data),非產品錯"
evidence: [03-login-after.png, notes.md, "../20260806-clean-add-to-cart/notes.md"]
- id: R-04
check: "登入後導覽列應顯示使用者名稱"
status: fail
expected: "顯示該帳號的姓名(對照組同一位置顯示 Jane Doe)"
actual: "顯示「User Data not found」,但同一頁 GET /users/me 回 200 OK。API 拿得到資料、畫面宣告查無使用者"
evidence: [08-account-after.png, "network.log#L5", "../20260806-clean-add-to-cart/03-account-after.png"]
- id: R-11
check: "註冊頁國家下拉選取後表單應成為 valid"
status: inconclusive
reason: "以 select_option 選取後 DOM value 已是 NL,Angular 控制項仍為 ng-pristine ng-invalid,送出被擋在「Country is required.」;手動 dispatch change/input 事件才轉 ng-valid。無法分離是產品的事件繫結問題,還是這支 CLI 的 select 實作差異——沒有以真人手動操作重跑一次的證據"
evidence: [notes.md, 04-register-before.png]
- id: R-12
check: "頁面對外的第三方請求"
status: anomaly
detail: "cdn-cgi/rum 的 beacon 多次 net::ERR_ABORTED(另有兩次回 204);屬 Cloudflare RUM 遙測,不在本輪受測範圍,只記錄不判定"
evidence: ["network.log#L3", "network.log#L4", "network.log#L13", "network.log#L14"]
第一個是登入被拒的案例。
這組帳密不是亂打的 —— 它在 clean 站登得進去(POST /users/login 回 200),到了 with-bugs 站卻回 401,畫面顯示「Invalid email or password」。


03-login-after.png:帳密被擋下,訊息與 401 一致。
但這一筆是 pass。因為產品該做的事都做對了:帳密在這個站不存在,它擋下來、訊息與 API 狀態碼一致。真正的原因是兩個站的種子資料不同,那是測資問題,不是缺陷。
這就是散文看不出來的地方。notes.md 裡它是一條「登入失敗」的觀察,讀起來像出事;填進 results.yaml 才顯示出它其實是通過。
第二個案例是一個 fail case:
今天我們在登入這個網站之後,帳號的姓名欄位應該要顯示「使用者名稱」,但今天發現它顯示的是 "User data not found"。


with-bugs 版:登入成功,導覽列說查無使用者。


乾淨版同一個位置顯示 Jane Doe。expected 那一欄寫得出「對照組顯示什麼」,靠的就是這張。沒有它,你只能寫「應該顯示姓名」,那是你的期待不是產品的承諾。
妙的是,在同一頁的時候,它拿到的 User API 其實是回傳 200 OK 的,這代表我們其實是有拿到資料的。像這種就是一致性的判斷問題,所以很明顯這就是一個 Bug。
第三個 R11 的 Case,我們在下拉國家的時候,它可能會發現它在拉的時候會被擋在「Country is required」。
這時候我們沒辦法去區分它到底是不是產品的問題,還是因為 Playwright CLI 的 selector 和人類去點選的行為不一樣。
有的時候我們必須把問題分離出來,可能真的需要有人去動手測試一次。
像這樣的錯誤,它就會被表示為 inconclusive。
最後一個案例是頁面對第三方發出的遙測請求。
頁面載入時會打 Cloudflare 的 cdn-cgi beacon,其中幾筆被中止(net::ERR_ABORTED),但這並不在我們要測的範圍之內。它只是一個頁面對於第三方的請求,所以這時候我們就會判斷成 anomaly。
然而這一輪總共有 12 個檢查點,你可以在 results.yaml 這個檔案裡面看到它們各自的結果。
我們可以統計出來,這一包證據下面其實是有:
pass
fail
inconclusive
anomaly
這四個數字是散文交不出來的東西。同一包證據、同一份 notes.md,人讀完只會得到「有幾個地方怪怪的」;填成 results.yaml 之後,它變成可以統計、可以跟上一輪比、可以交給下一支 skill 直接接手的輸入。
通過只有一種樣子,失敗有五種原因,把它們全塞進 fail 這一格,你就分不出該修產品、該修測資、該補證據,還是該先重跑幾次。所以結果要寫成六個狀態的結構化檔案,每一筆掛上 expected、actual 與指得到的 evidence。判不動的時候有兩條退路 —— 證據不足寫 inconclusive、不在守備範圍寫 anomaly —— 而不是硬塞進 pass 或 fail。
一包證據 + notes.md(散文)
│
▼
/sdet-skills:structured-result
│
▼
results.yaml
每筆:check|status|expected
actual|evidence
│
┌────────┬────────┼────────┬────────┬────────┐
▼ ▼ ▼ ▼ ▼ ▼
pass fail blocked flaky anomaly inconclusive
如預期 不符預期 前置沒成功 時好時壞 範圍外 證據不足
(含該擋 沒測到 要多跑 只記錄 要補證據
的有擋) 幾次才算 或找人手測
│ │ │ │ │ │
└────────┴────────┴───┬────┴────────┴────────┘
▼
數得出來、比得了上一輪
下一支 skill 直接吃
│
▼
本輪:4 pass / 6 fail / 1 inconclusive / 1 anomaly
通過只有一種,失敗有很多種,混成一格就失去分流的能力,而分流決定了誰該修。pass 不等於沒事,fail 也不等於產品壞了:那筆登入被拒是 pass(產品該擋有擋,是兩站種子資料不同),這件事讀散文看不出來。判不動就寫判不動,inconclusive 與 anomaly 是兩條正當的退路,硬判成 fail 只會讓下游多花一輪去推翻你。
今天我們根據在 explore 這些網站時所發現、錄下的證據,甚至將這些發生的行為跟證據連接,去判斷說這個行為是不是正常的。
如果是通過的話,那就代表這個行為符合預期。但如果今天這個行為不符合預期,其實它有大概 5 種不同的失敗原因。
明天我們就要把這些前面說的 results.yaml 去做分類,釐清測試案例失敗時,到底是哪一種原因造成的。
results.yaml 的祖宗:session sheet 規定一輪要交代涵蓋範圍、發現的問題與時間分配。2000 年的做法就已經不是只交 Pass/Fail。差別在讀者 —— session sheet 是寫給經理看的,results.yaml 是寫給下一支 skill 吃的