iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Software Development

同一套 Laravel 系統,測試怎麼寫才不會說謊系列 第 16

Day 16:Fake 測試替身設計——同一份錄影帶,兩支不同協定的 Fake 各自挑自己看得懂的部分播放

  • 分享至 

  • xImage
  •  

前言

「要 mock 兩種不同通訊協定的外部服務,是不是要各自準備一份假資料?」

這個系統要跟外部校務系統交換資料,走的是兩種不同的技術路徑:一般的 HTTP 請求,還有 SOAP 協議。如果各自準備各自的假資料來源,維護成本會隨著介面數量往上疊。今天要講這個系統怎麼解決這個問題——讓兩支負責不同協定的測試替身,共用同一份真實錄製下來的流量紀錄。

今日目標

  • 認識兩支自製測試替身:一支模擬 HTTP 客戶端,一支模擬 SOAP 呼叫器
  • 理解它們怎麼共用同一份錄製資料,卻各自處理不同格式
  • 看懂「依賴介面而不是具體實作」這件事,為什麼能讓測試替身變得可能
  • 建立設計測試替身時的判斷力:什麼時候該共用資料源,什麼時候該各自獨立

兩支介面不同、資料源相同的 Fake

這個系統裡有兩支自製的測試替身:一支實作了 PSR-18 的 ClientInterface(標準 HTTP 客戶端介面),另一支實作了 SOAP client 套件定義的 Caller 介面。這兩支替身都做同一件事——依序播放一份 YAML 格式的「錄影帶」,裡面記錄著先前真實呼叫外部系統時的完整 request/response。

有意思的地方在於,這兩支替身讀的是同一份錄影帶,卻各自用不同方式解讀裡面的內容。實作 HTTP 客戶端介面的那支,直接把錄影帶裡的回應內容包成標準的 HTTP Response 物件回傳;實作 SOAP 呼叫器介面的那支,要多一道手續——用正規表示式解析錄影帶裡的 SOAP XML 內容,抓出裡面對應的結果標籤,重新組裝成 SOAP client 套件期待的回傳格式。

對照一下這個設計解決了什麼問題

每種協定各自準備一份假資料

// HTTP 用的假資料
class FakeHttpClient
{
    private array $httpFixtures = [/* 一份 HTTP 專用的假資料 */];
}

// SOAP 用的假資料
class FakeSoapCaller
{
    private array $soapFixtures = [/* 另一份 SOAP 專用的假資料,跟上面沒有關聯 */];
}

兩份假資料各自維護,錄製一次真實流量之後,要手動整理成兩種不同的格式,如果外部系統的回應內容改了,要記得同步更新兩個地方。

共用同一份錄影帶,各自篩選自己看得懂的部分

class FakeHttpClient implements ClientInterface
{
    public function sendRequest(RequestInterface $request): ResponseInterface
    {
        $entry = $this->findEntry($request, requireSoapAction: false);
        return new Response($entry['response']['status'], [], $entry['response']['body']);
    }
}

class FakeCaller implements Caller
{
    public function __call($method, $arguments)
    {
        $entry = $this->findEntry(soapAction: $method);
        return $this->parseSoapResponse($entry['response']['body']);
    }
}

兩支替身都指向同一份 YAML 錄影帶,靠請求裡有沒有 SOAP 專屬的標頭(SOAPAction)互相過濾——同一批真實錄下來的流量,SOAP 呼叫自動被 FakeCaller 挑走,非 SOAP 呼叫自動被 FakeHttpClient 挑走。錄一次真實流量,兩種協定的測試替身都能用,維護的資料來源只有一份。

這個設計為什麼可能:依賴介面,而不是具體實作

這個共用設計能成立的前提,是系統裡呼叫外部服務的程式碼,本來就是對著介面寫的(ClientInterfaceCaller),而不是直接依賴某個特定的 HTTP client 套件或 SOAP client 套件。因為程式碼不在乎「實際執行的是哪一個實作」,只在乎「這個實作有沒有滿足介面的約定」,測試的時候才能直接把真實實作換成 Fake,不需要改動任何一行業務邏輯。這正是「打外部 API 一律用標準介面組 request,不要寫死特定 client」這種設計原則真正的好處——不是為了理論上的整潔,而是讓測試替身這種東西變得可能。

今日思考題

如果你的專案要跟多個外部系統整合,各自走不同的通訊協定,你手上的測試替身是各自維護各自的假資料,還是有辦法共用同一份來源?如果現在是各自維護,共用之後能省下多少重複的維護成本?

今日重點回顧

  • 這個系統有兩支測試替身,分別實作 HTTP 客戶端介面跟 SOAP 呼叫器介面
  • 兩支替身共用同一份 YAML 格式的真實錄製資料,靠請求特徵互相過濾
  • 這個設計能成立的前提是業務邏輯依賴標準介面,不是寫死某個特定套件的具體實作
  • 測試替身不一定要各自準備各自的假資料,用篩選條件切開職責是另一種選擇

明日預告

明天要把這份「錄影帶」機制攤開來看——23 個 cassette 檔案是怎麼分類組織的,檔名本身藏著什麼設計巧思。


上一篇
Day 15:附件測試——同一支 class 要吃三種舊系統資料格式,怎麼用 dataset 一次測完
系列文
同一套 Laravel 系統,測試怎麼寫才不會說謊16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言