前面幾天講的都是「怎麼用測試替身取代不可控依賴」,但沒講一個容易被忽略的問題:替身要包在哪一層介面上,本身也是一個設計決策——包錯層級,測試會變得極度脆弱,只要內部實作稍微調整,即使外部行為完全沒變,測試也會跟著壞掉。
以 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 物件)沒有改變,這個測試就不會因為內部重構而壞掉。
測試替身該包在「呼叫端跟這個依賴之間,真正約定好、不輕易改變的邊界」上,而不是包在「這個依賴目前剛好是怎麼實作的」這個細節上。 邊界通常對應到一個穩定的公開介面(例如 Http.get()),實作細節則是介面背後可能隨時調整的內部結構。包在邊界上的測試,只有在對外行為真的改變時才會需要跟著改,這才是測試該有的穩定性。
同樣的原則也適用在 Day 24 的 pyfakefs、Day 25 的加密測試——都是選擇在「Python 標準函式庫的公開介面」(open()、os.path)跟「加密函式庫的公開介面」(AES.new())這種穩定邊界上做替身,而不是深入 mock 這些函式庫內部怎麼實作。這正是為什麼這些替身策略能夠成立、而且不容易隨著重構壞掉的原因。
你寫過的 mock 測試裡,有沒有 mock 到私有方法、內部實作細節的例子?上次改動內部實作時,這些測試有沒有無緣無故壞掉?
明天是第四部回顧:三種不可控依賴,三種對應的替身策略,收斂成一張總表。