第二部看過 commit 80d3469「順路補測試」的真實案例——加新功能時,順便把舊路徑的測試缺口補上。今天反過來,不等有新功能當藉口,直接主動幫 RefundRequest 補一個例外情境測試,看看實際要花多少功夫。
提醒:以下操作都在我自己的本機暫存副本上做示範,不是對真實公開套件的正式修改。
RefundRequest 目前欠缺的一個具體例外情境validate() 機制,這個套件其實不用自己重寫驗證邏輯Day 05 已經核實過,RefundRequestTest 目前只有一個測試方法,只驗證 getData() 回傳的 Action 欄位是不是 'R'。但 RefundRequest::getData() 實際上做了兩件事:
public function getData()
{
$this->validate('transactionId', 'transactionReference', 'amount');
return [
'MerchantTradeNo' => $this->getTransactionId(),
'TradeNo' => $this->getTransactionReference(),
'Action' => 'R',
'TotalAmount' => $this->getAmount(),
];
}
第一行的 $this->validate(...) 從沒被測過——如果呼叫端忘了設定 transactionReference(也就是綠界的 TradeNo,要退款的交易編號),會發生什麼事?
validate() 不是這個套件自己寫的,是 Omnipay 官方 omnipay/common 套件在 ParametersTrait 裡提供的通用方法:
// omnipay/common 官方原始碼
public function validate(...$args)
{
foreach ($args as $key) {
$value = $this->parameters->get($key);
if (! isset($value)) {
throw new InvalidRequestException("The $key parameter is required");
}
}
}
邏輯很單純:逐一檢查傳進來的參數名稱有沒有被設定,沒設定就丟出 InvalidRequestException,訊息固定是 "The {參數名} parameter is required"。這代表 RefundRequest 完全不需要自己寫任何驗證邏輯,只要呼叫官方提供的 validate(),行為就已經是確定的——這也是為什麼補這個測試不需要猜測套件裡有什麼特殊邏輯,直接照官方框架的既定行為寫斷言就好。
✅ 新增到 RefundRequestTest.php
public function testMissingTransactionReferenceThrowsException()
{
$this->expectException(\Omnipay\Common\Exception\InvalidRequestException::class);
$this->expectExceptionMessage('The transactionReference parameter is required');
$request = new StubRefundRequest($this->getHttpClient(), $this->getHttpRequest());
$request->initialize([
'MerchantID' => '2000132',
'MerchantTradeNo' => '2821567410556',
'TotalAmount' => 1000,
// 故意不設定 TradeNo(對應 transactionReference)
]);
$request->getData();
}
在本機暫存副本上實際跑起來:
$ vendor/bin/phpunit --filter RefundRequestTest --testdox
✔ Get data
✔ Missing transaction reference throws exception
Tests: 2, Assertions: 3
兩個測試都通過。新增的這個測試方法,加上測試方法之間的空行,總共只花了 13 行程式碼。VoidRequest 因為 extends RefundRequest、共用同一個 validate() 呼叫,可以用完全一樣的模式再補一個對應測試——這裡不重複貼程式碼,邏輯跟上面一模一樣,只是換一個 Stub 類別。
❌ 等待型
「這個路徑總有一天會因為別的原因被回頭改到,到時候順便補測試。」
(第二部看到的 commit 80d3469 就是這樣發生的,但那次補到的是 ATM/彈性分期,Refund/Void 至今還沒輪到)
✅ 主動型
「不需要理由,直接找出目前測試覆蓋最薄弱的一段程式碼,排時間補上去。」
(今天這 13 行,不需要等任何新功能當藉口)
兩種方式都能讓覆蓋率變好,差別在於等待型完全看運氣——這個路徑會不會剛好在未來某次改動裡被順路照顧到,沒人能保證。主動型不需要運氣,只需要有人願意主動花 13 行程式碼的時間。測試覆蓋不均的問題,通常不是因為補一個測試很難,而是因為沒有人主動去排這件事的優先順序。
如果請 AI 幫你做「找出這個套件裡測試覆蓋最薄弱的一段程式碼,並補一個例外測試」這件事,你覺得它做得到嗎?它需要什麼資訊才能判斷「薄弱」——這正是明天要討論的 prompt 設計問題。
RefundRequest::getData() 裡的 validate() 呼叫,從沒被測過validate() 是 Omnipay 官方框架提供的通用機制,行為已經確定,補測試不需要猜測特殊邏輯明天把這幾天累積的觀察收斂成一份具體的 CLAUDE.md 規則文字:怎麼要求 AI 在補功能的同時,主動檢查同一個檔案裡還有沒有其他測試覆蓋的缺口。