Mockito 中的泛型列表匹配器
1. 概述
在使用 Mockito 編寫 Java 單元測試時,我們經常需要對接受泛型List
參數的方法進行存根,例如List<String>
、 List<Integer>
等。
在本教程中,我們將探討如何在 Mockito 中使用具有泛型的List
匹配器。我們將介紹 Java 7 和 Java 8+ 的案例。
2.問題介紹
首先,我們透過一個例子來了解泛型和 Mockito 挑戰。
假設我們有一個帶有預設方法的介面:
interface MyInterface {
default String extractFirstLetters(List<String> words) {
return String.join(", ", words.stream().map(str -> str.substring(0, 1)).toList());
}
}
extractFirstLetters()
方法接受一個String
值List
,並傳回一個以逗號分隔的String
,其中包含List.
現在,我們想在測試中模擬MyInterface
並存根extractFirstLetters()
方法:
MyInterface mock = Mockito.mock(MyInterface.class);
when(mock.extractFirstLetters(any(List.class))).thenReturn("a, b, c, d, e");
assertEquals("a, b, c, d, e", mock.extractFirstLetters(new ArrayList<String>()));
在這個例子中,我們只是使用ArgumentMatchers.any(List.class)
來匹配通用List
參數。如果我們運行測試,它就會通過。因此,存根按預期工作。
但是,如果我們檢查編譯器日誌,我們會看到一個警告:
Unchecked assignment: 'java.util.List' to 'java.util.List<java.lang.String>'
這是因為我們使用any(List.class)
來匹配通用List<String>
參數。編譯器無法在編譯時驗證原始List
是否僅包含String
元素。
接下來,讓我們來探索存根方法和匹配通用List
參數的正確方法。由於 Java 7 和 Java 8+ 中類型推斷的處理方式不同,因此我們將介紹 Java 7 和 Java 8+ 兩種情況。
3. Java 7 中匹配泛型List
參數
有時,我們必須處理具有舊 Java 版本的遺留 Java 專案。
在 Java 7 中,類型推斷受到限制,因此當使用像anyList().
這樣的ArugmentMatchers
時,編譯器很難確定正確的泛型類型。因此,在使用Mockito的**ArgumentMatchers.**
時我們必須指定泛型類型。
接下來,讓我們看看如何在 Java 7 中對extractFirstLetters()
方法進行存根:
// Java 7
MyInterface mock = Mockito.mock(MyInterface.class);
when(mock.extractFirstLetters(ArgumentMatchers.<String>anyList())).thenReturn("a, b, c, d, e");
assertEquals("a, b, c, d, e", mock.extractFirstLetters(new ArrayList<>()));
如測試所示,我們在anyList()
匹配器上指定了<String>
類型。測試編譯並通過。
如果不明確指定<String>,
anyList()
將傳回List<?>,
這與預期的List<String>
不匹配,且 導致 Java 7 中的編譯器錯誤。
4. 在 Java 8+ 中比對泛型List
參數
在 Java 8 及更高版本中,編譯器變得更加聰明。我們不必明確指定型別參數。
因此,我們可以簡單地使用anyList()
而不指定<String>
,編譯器就會正確推論出預期的型別:
MyInterface mock = Mockito.mock(MyInterface.class);
when(mock.extractFirstLetters(anyList())).thenReturn("a, b, c, d, e");
assertEquals("a, b, c, d, e", mock.extractFirstLetters(new ArrayList<>()));
如果我們執行測試,它會通過並且不會出現編譯器警告。這是因為Java 8+ 編譯器會自動從**extractFirstLetters(List<String>)**
推斷出泛型類型。
我們中的一些人可能會想出一種使用any()
匹配器來匹配所需的通用參數的方法來:
MyInterface mock = Mockito.mock(MyInterface.class);
when(mock.extractFirstLetters(any())).thenReturn("a, b, c, d, e");
assertEquals("a, b, c, d, e", mock.extractFirstLetters(new ArrayList<>()));
程式碼看起來緊湊且簡單。類似地,Java 8+ 編譯器可以從目標方法推斷出泛型類型,因此這種方法也有效。但是, any()
是一個通用匹配器,可以匹配任何物件。它不太特定於類型,並且可能導致匹配器不太精確的情況。
實際上,對於明確採用List<String>
的方法,
使用anyList()
會更精確且自文檔化,清楚地表示匹配器需要List.
因此,儘管從技術上來說兩種匹配都可以使用,但在處理List
參數時,為了獲得更好的類型安全性和可讀性,最好使用anyList()
。
5. 結論
Mockito 的List
匹配器可以輕鬆使用通用List
參數,但我們需要注意類型擦除和 Java 的類型推論。
在本文中,我們探討如何正確搭配通用List
參數:
- 在 Java 7 中 – 我們必須明確指定泛型類型:
ArgumentMatchers.<T>anyList()
- 在 Java 8 及更高版本中 - 我們可以直接使用
anyList()
而無需明確指定<T>
,因為編譯器可以自動推斷類型
理解這些概念使我們能夠在傳統和現代 Java 專案中編寫更清晰、更有效的單元測試。
與往常一樣,範例的完整原始程式碼可在 GitHub 上找到。