iT邦幫忙

2026 iThome 鐵人賽

DAY 28
0
Software Development

下載器不是寫完就能跑:一個 HLS 分段式串流工具的架構與測試修練系列 第 28 篇

Day 28:測試替身選錯層——mock 太底層,改一次實作就要改一次測試

  • 分享至 

  • xImage
  •  

前言:mock 應該包在哪一層?

前面幾天講的都是「怎麼用測試替身取代不可控依賴」,但沒講一個容易被忽略的問題:替身要包在哪一層介面上,本身也是一個設計決策——包錯層級,測試會變得極度脆弱,只要內部實作稍微調整,即使外部行為完全沒變,測試也會跟著壞掉。

今日目標

  • 認識「mock 太底層」的具體症狀
  • 看一組「mock 內部實作細節 vs mock 穩定的抽象介面」的對照
  • 理解測試替身該包在「行為的邊界」,不是「實作的細節」

❌ vs ✅:mock 內部細節 vs mock 穩定的邊界

以 Day 23 的 HTTP 呼叫為例:

❌ 反例:mock 到 client library 內部使用的私有方法

@patch.object(Http, '_build_session')  # mock 到內部實作細節
async def test_download(mock_build_session):
    mock_build_session.return_value = fake_session
    ...

如果 Http 類別的內部實作有一天從「自己組 session」改成用別的方式管理連線,_build_session 這個方法可能整個消失或改名——即使 Http.get() 對外的行為完全沒變,這個測試也會因為 mock 不到而失敗。測試在驗證一個跟外部行為無關的實作細節,這種脆弱是不必要的。

✅ 正例:mock 到穩定、對外公開的介面邊界

@patch.object(Http, 'get')  # mock 到公開介面,不管內部怎麼實作
async def test_download(mock_get):
    mock_get.return_value = MockResponse(content, headers)
    ...

不管 Http.get() 內部是用 aiohttp、httpx,還是自己手刻的 session 管理,只要對外的介面(傳入網址、回傳一個 response 物件)沒有改變,這個測試就不會因為內部重構而壞掉。

判斷準則:這個 mock 點,是不是這一層真正該對外承諾的邊界

測試替身該包在「呼叫端跟這個依賴之間,真正約定好、不輕易改變的邊界」上,而不是包在「這個依賴目前剛好是怎麼實作的」這個細節上。 邊界通常對應到一個穩定的公開介面(例如 Http.get()),實作細節則是介面背後可能隨時調整的內部結構。包在邊界上的測試,只有在對外行為真的改變時才會需要跟著改,這才是測試該有的穩定性。

這個判斷也適用在檔案系統跟加密

同樣的原則也適用在 Day 24 的 pyfakefs、Day 25 的加密測試——都是選擇在「Python 標準函式庫的公開介面」(open()、os.path)跟「加密函式庫的公開介面」(AES.new())這種穩定邊界上做替身,而不是深入 mock 這些函式庫內部怎麼實作。這正是為什麼這些替身策略能夠成立、而且不容易隨著重構壞掉的原因。

今日思考題

你寫過的 mock 測試裡,有沒有 mock 到私有方法、內部實作細節的例子?上次改動內部實作時,這些測試有沒有無緣無故壞掉?

今日重點回顧

  • 測試替身包錯層級(包到內部實作細節),會讓測試對不影響外部行為的重構過度敏感
  • 該包在「穩定、對外公開的介面邊界」,不是「目前剛好怎麼實作」的細節
  • 這個判斷準則同樣適用在網路、檔案系統、加密——一致選擇在穩定邊界上做替身

明日預告

明天是第四部回顧:三種不可控依賴,三種對應的替身策略,收斂成一張總表。


上一篇
Day 27:案例——一個看似綠燈的測試,其實沒斷言到真正關鍵的行為
下一篇
Day 29:第四部回顧——三種不可控依賴,三種對應的替身策略
系列文
下載器不是寫完就能跑:一個 HLS 分段式串流工具的架構與測試修練 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言