為什麼我們不應該用 Mockito 來嘲諷收藏集
1. 概述
Mockito 是 Java 生態系中最受歡迎的測試庫之一。我們用它來隔離程式碼單元、取代外部依賴項,並編寫快速且重點突出的測試。然而,Mockito 經常被過度使用,其中最常見的誤用之一就是模擬 Java 集合,例如List 、 Set或Map 。
乍一看,模擬集合似乎無害。 Mockito 允許這樣做,測試也能編譯通過,一切看起來都很正常。然而,隨著時間的推移,這種做法會導致測試變得脆弱、行為不切實際,並降低測試的可靠性。在較新的 Java 版本中,它甚至會導致測試失敗。
在本教學中,我們將探討為什麼模擬集合會有問題,以及我們應該怎麼做。
2. 集合並非我們的依賴項
在嘲諷任何事物之前,我們應該先確定它是否真的是依賴項。依賴項通常是外部的、速度較慢的或不確定的,例如資料庫、REST 用戶端、訊息代理程式或系統時鐘。 Java 集合不屬於這一類。它們是 JDK 核心元件的一部分,具有確定性、輕量級,並且經過了廣泛的測試。
當我們模擬一個集合時,我們並沒有將程式碼與外部行為隔離。相反,我們用人工存根取代了定義完善的 Java 行為。這增加了複雜性,卻沒有提供有效的隔離。
3. 範例程式碼
我們先從一個簡單的服務類別開始,該類別內部使用集合:
public class UserService {
private final List<String> users;
public UserService(List<String> users) {
this.users = users;
}
public boolean hasUsers() {
return !users.isEmpty();
}
public String getFirstUser() {
if (users.isEmpty()) {
return null;
}
return users.get(0);
}
}
這裡的List純粹是一種資料結構,它不代表外部協作者或任何會產生副作用的依賴關係。
4. 嘲諷收藏
真實的集合會維護一個內部狀態。當我們添加元素時,它的大小會自動改變。而模擬的集合則沒有這種行為,除非我們明確地定義它。這種差異常常導致測試雖然通過,但並不能反映真實的運行時行為。
在新版本的 Java 中,這種情況會變得更加棘手。
以下測試旨在演示模擬集合時出現的問題。在現代 Java 版本中,此測試甚至可能在運行之前就失敗:
@Test
void shouldFailToMockCollection_onModernJavaVersions() {
// This line may fail on Java 21+ due to JVM restrictions
List<String> users = mock(List.class);
UserService userService = new UserService(users);
// The test may never reach this point
userService.hasUsers();
}
在 Java 21 及更高版本中,Mockito 可能會拋出類似如下的異常:
Mockito cannot mock this class: interface java.util.List
Could not modify all classes [Iterable, SequencedCollection, Collection, List]
此故障並非由錯誤的測試邏輯引起,而是由於 JVM 對核心 JDK 類型運行時檢測規則更加嚴格。
5. 測試變得過於具體且脆弱
即使技術上可以模擬集合,測試也往往與實作細節緊密耦合。它們通常驗證isEmpty()或get(0)被呼叫了多少次,而不是專注於可觀察的行為。即使功能保持不變,一些小的內部重構也可能導致測試失敗。
好的單元測試應該要保護重構,而不是阻礙重構。
6. 使用實體收藏代替
使用真實收藏可以避免所有這些問題:
@Test
void givenList_whenRealCollectionIsUsed_thenShouldReturnFirstUser() {
List<String> users = new ArrayList<>();
users.add("Joey");
UserService userService = new UserService(users);
assertTrue(userService.hasUsers());
assertEquals("Joey", userService.getFirstUser());
}
該測試在各個 Java 版本中都能穩定運行,無需任何樁程式碼,並且能夠準確地反映生產環境中的行為。
使用真實資料集也更容易測試極端情況:
@Test
void givenEmptyList_whenUserListIsEmpty_thenShouldReturnNull() {
List<String> users = new ArrayList<>();
UserService userService = new UserService(users);
assertNull(userService.getFirstUser());
}
該測試清晰地記錄了預期行為,而無需依賴 Mockito。
需要模擬集合通常意味著存在更深層的設計問題,例如邏輯耦合過強或職責不明確。在測試中使用真實集合往往能暴露這些問題,並有助於編寫更簡潔的 API 和更好地分離關注點。
7. 結論
本文中我們看到,模擬 Java 集合通常不是個好主意。它會導致測試脆弱、行為不切實際,以及與實作細節不必要的耦合。在現代 Java 版本中,由於 JVM 的限制更加嚴格,它甚至可能導致執行時間失敗。
Mockito 如果使用得當,仍然是一款強大而有價值的工具。它應該模擬真實的協作物件和外部依賴項,而不是核心資料結構。透過在測試中使用真實的集合,我們可以編寫出更清晰、更安全、更能適應變化的程式碼。
與往常一樣,本文中提供的程式碼可在 GitHub 上找到。