iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
IT Operation

AI 輔助開發下,測試如何保住品質防線系列 第 5

Day 05:21 個測試方法,哪裡測得細、哪裡只測了半套

  • 分享至 

  • xImage
  •  

前言:「有寫測試」跟「測得夠不夠」是兩回事

「有寫測試」跟「測得夠不夠」是兩回事。今天把 omnipay-ecpay 全部 21 個測試方法攤開來數一遍,看清楚哪裡是真的紮實、哪裡只是「至少測過一次」而已。

今日目標

  • 知道 21 個測試方法在 6 個測試類別間怎麼分布
  • 找出這個套件裡測得最紮實的一段程式碼,以及為什麼
  • 理解「測試數量」跟「測試涵蓋的情境種類」不是同一件事
  • 看懂 testInvalidCheckMacValue 這個案例在防什麼

21 個測試方法,怎麼分布

測試類別 測試方法數 涵蓋情境
PurchaseRequestTest 5 信用卡 / ATM / BNPL 無卡分期 / 信用卡彈性分期,共 4 種付款方式 + 一個組資料的基礎案例
GatewayTest 6(另外自動繼承 26 個泛用測試,見下方說明) Gateway 層對 6 個方法(purchase/completePurchase/acceptNotification/fetchTransaction/refund/void)各委派一次
AcceptNotificationRequestTest 3 正常通知 + 委派後的欄位轉換 + 簽章驗證失敗
CompletePurchaseRequestTest 3 正常回傳 + 委派後的欄位轉換 + 簽章驗證失敗
RefundRequestTest 1 只驗證 Action 欄位固定回傳 'R'
VoidRequestTest 1 只驗證 Action 欄位固定回傳 'N'

光看這張表就能發現落差:PurchaseRequest 一個類別就佔了 5 個測試方法,涵蓋 4 種付款情境;RefundRequestVoidRequest 各自只有 1 個測試,而且都只驗證同一件事——送出的資料裡 Action 欄位有沒有帶對值,完全沒有驗證「金額是 0 怎麼辦」「退款失敗會怎麼樣」這類邊界或例外情境。

// RefundRequestTest.php 唯一的測試方法
public function testGetData()
{
    $request = new RefundRequest($this->getHttpClient(), $this->getHttpRequest());
    $request->initialize([/* ... */]);

    $data = $request->getData();

    self::assertEquals('R', $data['Action']);
    self::assertEquals('1000', $data['TotalAmount']);
}

補充一點:GatewayTest 之所以在表格裡看起來只有 6 個,是因為它繼承了 Omnipay 官方測試框架(omnipay/tests)提供的 GatewayTestCase 基底類別,實際本地跑 phpunit --list-tests 會發現 GatewayTest 底下總共執行 32 個測試方法——6 個是套件自己寫的,另外 26 個(testSupportsPurchasetestDefaultParametersHaveMatchingMethods 這類)是基底類別透過反射自動產生的泛用測試,任何 Omnipay 驅動套件只要繼承這個基底類別,都不用自己動手就先擋住一整層最基本的介面規範。全套件實際執行的測試數字是 47 個(21 個自己寫的 + 26 個繼承來的),比只算自己寫的 21 個更能反映這個套件真正被驗證過的範圍。

同一個套件裡,兩種程式碼受到的測試保護程度可以差這麼多——不是因為 Refund/Void 比較不重要(退款失敗對使用者的傷害通常比訂單查詢更直接),而是因為它們沒有像 Purchase 那樣,隨著多種付款方式的需求一直被回頭補測試。

這個套件測得最紮實的一段:防偽造付款通知

如果只能挑一個地方說「這裡的測試真的做對了」,是 AcceptNotificationRequestTestCompletePurchaseRequestTest 裡的 testInvalidCheckMacValue

綠界在付款完成後,會用背景通知(Server 對 Server)告訴你的系統「這筆訂單付款成功了」,通知資料裡會帶一個 CheckMacValue——用你的 HashKey/HashIV 對其他欄位算出來的簽章。如果有人想偽造一筆「付款成功」的假通知,因為不知道你的 HashKey/HashIV,算不出正確的 CheckMacValue,理論上會被擋下來。這個套件確實測了這件事:

public function testInvalidCheckMacValue()
{
    $this->expectException(InvalidRequestException::class);
    $this->expectExceptionMessage('CheckMacValue verify fail');

    $data = [
        // ...其餘欄位跟正常通知一模一樣
        'CheckMacValue' => '7EC8DDC6C5C51B1A4D8BEA261246066858B38184C55FD3DD3D6DFF53F535A64',
        // 正確值應該是 E7EC8DDC6C5C51B1A4D8BEA261246066858B38184C55FD3DD3D6DFF53F535A64,這裡故意少了開頭一個字元
    ];

    $this->getHttpRequest()->request->add($data);
    $notification = new AcceptNotificationRequest($this->getHttpClient(), $this->getHttpRequest());
    $notification->initialize([
        'HashKey' => '5294y06JbISpM5x9',
        'HashIV' => 'v77hoKGq4kWxNNIS',
        'EncryptType' => '1',
        'MerchantID' => '2000132',
    ]);
    $notification->setTestMode(true);
    $notification->getTransactionStatus();
}

補充:這裡的 HashKey/HashIV/MerchantID 不是外洩的機敏資料,是綠界官方文件公開提供給所有開發者測試環境使用的固定測試帳號參數。

這個測試沒有測「正常情況」,測的是刻意把簽章弄錯一個字元,確認程式真的會擋下來、而且擋下來的方式是拋出明確的 InvalidRequestException,訊息講清楚是簽章驗證失敗。這種「刻意破壞輸入,驗證系統有沒有正確拒絕」的測試,比十個「正常情況能跑過」的測試更有價值——正常情況本來就該過,例外路徑才是真正容易被漏掉、也最需要被驗證的地方。

為什麼安全關鍵路徑測得比較細,Refund/Void 卻沒有?

一個合理的推測(不是這個套件維護紀錄裡明講的動機,只是觀察現象):CheckMacValue 驗證失敗如果沒擋住,後果是任何人都能偽造一筆付款成功通知,讓系統誤以為訂單已付款——這是資安等級的風險,容易被開發者本能地意識到「這個一定要測」。而 RefundRequest/VoidRequest 目前只測了 Action 欄位,遺漏的邊界情況(金額異常、API 回傳失敗)造成的後果比較間接,也比較容易被視為「以後有空再補」。

測試覆蓋往往不是均勻分布的,而是跟著開發者當下感受到的風險強度走——風險感受得到的地方測得細,風險感受不到或還沒發生過的地方,測試就容易停在「至少測過一次」。

❌ vs ✅:只看測試「有沒有」,還是看測試「涵蓋了什麼」

❌ 只用「有沒有寫測試」當品質指標
RefundRequestTest ✓ 有測試
VoidRequestTest ✓ 有測試
AcceptNotificationRequestTest ✓ 有測試
→ 三個類別看起來都「有測試保護」,品質狀態一樣
✅ 追問「測試涵蓋了哪些情境」
RefundRequestTest:1 個測試,只驗證欄位值 → 覆蓋 1 種情境
VoidRequestTest:1 個測試,只驗證欄位值 → 覆蓋 1 種情境
AcceptNotificationRequestTest:3 個測試,含正常流程 + 簽章驗證失敗
→ 覆蓋 2 種以上情境,包含資安關鍵的例外路徑
→ 同樣「有測試」,實際被驗證過的風險範圍差很多

「這個類別有測試」不是一個及格/不及格的二元問題,追問「測試涵蓋了哪些情境」才看得出真正的差距。

今日思考題

你自己專案裡「測得最細」跟「測得最鬆」的兩個模組分別是什麼?测得細的那個,是因為真的重要,還是只是因為出過事、被嚇過一次?

今日重點回顧

  • 21 個測試方法裡,PurchaseRequest 佔 5 個涵蓋 4 種付款方式,Refund/Void 各只有 1 個只測欄位值
  • testInvalidCheckMacValue 是這個套件測得最紮實的部分:刻意破壞簽章,驗證系統正確拋出例外
  • 例外/邊界情境的測試,比正常路徑的測試更能證明系統真的擋得住風險
  • 測試覆蓋不均通常跟著「開發者當下感受到的風險強度」走,不是均勻分布

明日預告

明天看這個套件的測試替身怎麼做——沒有用 Mock/Mockery,而是用繼承覆寫的 Stub 類別,這個選擇背後的取捨是什麼。


上一篇
Day 04:文件停在骨架範本,程式碼卻已經走了三年
系列文
AI 輔助開發下,測試如何保住品質防線5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言