Appearance
第 26 天|替購物車寫支自動化測試
前言
昨天,我們修好了一個問題,也留下測試,方便以後再檢查。那前幾天找到的其他問題呢?如果每次都要靠人重新點一遍,時間一久,很容易就漏掉了。
所以今天,我們挑購物車來練習。還記得那個金額問題嗎?每一列都顯示 $00.00,頁尾總計卻是對的。帳有算,明細看起來倒像全店招待。
我們要請助手把「每列金額應該等於單價乘以數量」寫成測試。以後再遇到同樣的錯,就讓測試提醒我們。
開始之前
不過,先別急著叫助手寫。這次會選購物車,是因為前面已經查過、也請另一位獨立驗證過,我們知道哪裡不對、正確結果應該是什麼。拿這樣的問題來寫測試,才有東西可以核對。
如果還沒搞清楚要驗什麼,就先產出一大批測試,後面可能得一支一支回頭查。測試全部通過,心情會很好,但也得知道它到底檢查了什麼。
接著,我們把前面準備好的產品設定一起交給助手,讓他知道受測網址、登入方式和測試目錄。少哪一項,就先補哪一項。
今天用的技能叫 test-author,負責撰寫測試,需要由你主動呼叫。畢竟每多一支,往後就多一支要維護;助手幫忙寫,我們負責決定要不要留下來。
動手試試
準備好後,在配套專案 sdet-skills/ 開啟助手對話。確認本書的技能已安裝,就可以把下面這段貼進輸入框:
text
/sdet-skills:test-author 請把購物車的「每列金額=單價×數量」寫成自動化測試。
先讀既有問題證據、測試風格設定與共用操作,確認正確行為。
先查有沒有同樣的測試;有就沿用並檢查,不要再新增一支。
完成後分別跑乾淨版與含缺陷版,交回測試檔、結果和失敗證據。如果你接著前面的對話操作,助手通常已經知道是哪個問題。換了對話,就把檔案位置一起補上。下面這些路徑都從 sdet-skills/ 算起;你的副本若沒有那份歷史報告,就換成自己保存的證據:
text
問題證據:output/reports/issues/2026-07-30-cart-line-total-zero.md
測試風格範本:config/test-style.example.md
預設測試原則:references/test-design.md
共用操作:tests/fixtures/sut.ts
購物車測試:tests/e2e/cart-line-total.spec.ts
執行設定:tests/playwright.config.ts資料交出去後,助手會先確認頁面上的元素怎麼找,再寫檢查結果的斷言。等測試準備好,你也可以在終端機自己跑:
bash
npm run test:clean
npm run test:with-bugs這兩行都在 sdet-skills/ 執行,用的是同一份測試碼。差別在於 SUT 環境變數:clean 連到 Toolshop 乾淨版,with-bugs 連到刻意放入缺陷的版本。這次測的是兩個公開網站,和昨天修程式用的本機環境不同。
跑完之後
我們先看看助手交回的測試。整套示範有四支,兩支驗登入、兩支驗購物車。下面只看其中一支購物車測試,共用操作放在 tests/fixtures/sut.ts:
ts
test('購物車每一列的 Total 應該等於單價乘以數量', async ({ page }) => {
const { name, price } = await openFirstProduct(page)
await addToCart(page)
await gotoCart(page)
const row = page.locator('tbody tr').filter({ hasText: name }).first()
const qty = Number(await row.locator('[data-test="product-quantity"]').inputValue())
const unit = money(await row.locator('[data-test="product-price"]').innerText())
const line = money(await row.locator('[data-test="line-price"]').innerText())
expect.soft(unit, '商品頁與購物車的單價應該一致').toBeCloseTo(price, 2)
expect(line, `${name}:${unit} × ${qty} 應該等於列總計(SUT=${SUT})`).toBeCloseTo(unit * qty, 2)
})這支測試先讀出商品、單價和數量,再核對那一列的金額。它沒有把某件商品的價格當成固定答案,所以換了商品,也能照同一個算法檢查。當時四支一起跑完,得到這個結果:
text
SUT=clean 4 passed
SUT=with-bugs 2 passed, 2 failed乾淨版四支都通過,含缺陷版則有兩支失敗。其中另一支購物車測試,會把每列金額加起來,再和頁尾總計比較。它留下的訊息是:
text
Error: 列總計相加 0 應該等於頁尾 14.15(SUT=with-bugs)
Expected: 14.15
Received: 0
看到兩邊結果不同,我們還要回頭對證據。這次頁面有正常載入,錯誤金額也和前面的探索、獨立驗證對得上,才有理由判斷是產品問題。光看一邊通過、一邊失敗,還不能直接下結論。
你也可以試著只讀那句失敗訊息:哪個版本、算出多少、應該多少,都在裡面。下一位同事拿到,就能接著查,不必先把整支程式看完。
背後怎麼做
既然測試要留下來給大家用,我們就順便檢查它好不好維護。下面這五條,可以在看助手交回的程式時一起核對:
| 寫法 | 為什麼這樣做 |
|---|---|
| 一支測試驗一個行為 | 失敗時容易縮小範圍,不把登入到結帳全塞在一起 |
| 名稱寫出使用者期待的結果 | 「列金額應等於單價乘數量」比「點擊按鈕」有用 |
| 先找測試專用屬性,再看角色與文字 | 避免依賴容易變動的版面位置 |
| 等到條件成立才往下走 | 不用固定睡幾秒賭畫面已經好了 |
| 從頁面或測資取得會變動的值 | 不把商品名、價格和編號寫死 |
先說怎麼找元素。本篇用 tests/tools/probe-selectors.ts 查過頁面,才知道有哪些 data-test 可以用;換到別的專案,也要先查,別照名字猜。
接著是等待。Playwright 的網頁斷言會反覆檢查條件,但上面的 toBeCloseTo 拿到的是已經讀出的數字,不會回頭重讀頁面。所以我們得先等畫面準備好,再讀值比較。斷言的官方說明
測資也一樣,先準備好再跑。需要固定資料,就建立帶有唯一標記的一份,用完只清掉自己建的部分;帳密則從環境變數取得。這個專案沒設登入帳密時,會跳過相關測試,報告要把跳過幾支寫出來。
到這裡,我們再站在接手同事的角度看一次:失敗訊息看得懂嗎?資料會自己準備、用完清掉嗎?這支能單獨跑,還是非得先跑另一支?如果已經有相同的測試,也就不用再養一支。
最後還可以故意把預期值改錯,看看它會不會失敗,檢查完再還原。
這個小動作是在確認斷言真的有跑到。還原後,乾淨版應該通過;已知有缺陷的版本仍可能失敗,那就保留失敗結果,繼續對照產品問題。
至於名稱怎麼取、頁面怎麼切,就照團隊的測試風格設定。現行技能會讀 config/<project>/test-style.md,例如 config/toolshop/test-style.md;可以參考配套範本建立。還沒設定時,先沿用 references/test-design.md。縮排、引號這些小事,交給格式檢查工具就好。
今天學到什麼
今天,我們把購物車的已知問題寫成測試,再用兩個版本核對它有沒有抓對地方。交付前也順手確認:沒有重複、能單獨跑,失敗時別人看得懂。
接下來還差一步:讓它記得自己跑。明天我們就接上 GitHub Actions,程式更新時自動檢查,報告和證據也一起留下。
作者備忘錄 — 正式出版前應移除
章節大綱
- 前言 — 從回歸測試接到日常檢查。
- 開始之前 — 先確認正確行為,再由人決定新增測試。
- 動手試試 — 提供技能提示詞、具體路徑與執行指令。
- 跑完之後 — 區分單支程式節錄、四支套件與兩環境結果。
- 背後怎麼做 — 等待、測資、帳密與交付檢查。
- 今天學到什麼 — 接到自動執行與留證。
參考資料
- Gerard Meszaros(2007)《xUnit Test Patterns: Refactoring Test Code》 - 測試碼本身也是要維護的資產,書裡的 test smell 目錄正是五條紀律的來源
- Playwright Docs — Best Practices - user-facing locator、web-first assertion、避免測試互相依賴的官方立場
- Playwright Docs — Auto-waiting - 為什麼 web-first assertion 可以取代固定等待
- Martin Fowler(2011)《Eradicating Non-Determinism in Tests》 - 不確定的測試比沒有測試更糟,第 4、5 條紀律的依據
- 本專案
references/test-design.md、config/test-style.example.md(專案自己的測試風格)、tests/e2e/cart-line-total.spec.ts- 本篇的實測主角