「要測試一段字串清洗邏輯,是不是要把清洗前後的每個字元都比對過一次?」
今天要看的是這個系統處理外部系統傳回的髒 XML 資料時,用的一種很精簡的測試手法:不逐欄位比對清洗前後的完整內容,而是只驗證一件事——「清洗過後的字串裡,不該再出現的東西,真的不見了」。這種「用症狀驗證」的方式,反而比鉅細靡遺比對每個字元更穩固。
外部系統回傳的資料裡,偶爾會夾雜一些不合法的 XML 字元——例如某些控制字元被編碼成  這種 HTML entity 格式,如果直接拿去解析 XML,會讓解析器出錯甚至整段資料處理失敗。這個系統有一段字串清洗邏輯,專門處理這種情況:先把這類 HTML entity 編碼的字元轉換回原本的 UTF-8 字元,再用正則加上逐字元檢查的方式,把不合法的控制字元跟殘留的無效 UTF-8 序列過濾掉。
❌ 逐欄位比對清洗前後的完整內容
test('sanitizes xml string', function () {
$dirty = '<data>王小明  02-1234-5678  test@example.edu</data>';
$clean = sanitizeXML($dirty);
expect($clean)->toBe('<data>王小明 02-1234-5678 test@example.edu</data>');
});
這種寫法要準確預測清洗過後每一個字元會變成什麼樣子,一旦清洗邏輯有任何微調(例如改成保留一個空白而不是完全刪除),這支測試就要跟著改斷言內容,維護成本高,而且如果測試資料裡放了看起來像真實個資的內容,還會帶來額外的去識別化風險。
✅ 只驗證「不該出現的東西真的不見了」
test('strips invalid xml characters', function () {
$dirty = '<data>' . str_repeat('sample text ', 3) . '</data>';
$clean = sanitizeXML($dirty);
expect($clean)->not->toMatch('/&#x(\d+);/');
});
不管清洗邏輯的實作細節怎麼調整,這支測試只問一個問題:「清洗完的字串裡,還找不找得到這種需要被清掉的 pattern」。清洗邏輯內部要用轉碼、正則、還是逐字元檢查來達成,測試完全不在乎,只在乎最終症狀有沒有消失——這讓測試對實作細節的變動更有彈性,同時也不需要在測試資料裡放進任何看起來像真實資料的內容。
查這段清洗邏輯的版本歷史,只能追到一次「升級時把檔案搬過去」的紀錄,找不到最初撰寫這段邏輯的原始 commit。程式碼裡的註解直接附上了一篇公開技術文章的連結,說明這個處理 XML 非法字元的手法是站在別人已經踩過的坑上,不是這個團隊自己憑空發明的解法。誠實承認「這裡抄的是公開資源」,比暗示這是原創解法更貼近真實情況——遇到別人已經解決過的通用問題,直接引用已驗證過的做法,本來就是合理的選擇,不需要為了塑造原創性硬掰一套自己的說法。
回想你手上專案裡有沒有類似「清洗字串、過濾不合法內容」這類防禦性邏輯,測試是逐欄位比對完整輸出,還是只驗證「不該出現的東西真的不見了」?如果實作細節之後要調整,哪一種測試改起來的成本比較低?
明天要講一次真實發生過的測試修正:拿掉一支手動塞假資料的 unit test,換成真的執行上傳流程的整合測試,為什麼後者才真正可信。