使用 Mockito 模擬嵌套方法調用
1. 概述
在本教程中,我們將了解如何使用 Mockito 存根(特別是深度存根)模擬巢狀方法呼叫。要了解有關使用 Mockito 進行測試的更多信息,請查看我們全面的 Mockito 系列。
2. 解釋問題
在複雜的程式碼中,尤其是遺留程式碼中,有時很難初始化單元測試所需的所有物件。很容易引入許多我們在測試中不需要的依賴項。另一方面,模擬這些物件可能會導致空指標異常。
讓我們來看一個程式碼範例,我們將在其中探討這兩種方法的局限性。
對於兩者,我們都需要一些類別來測試。首先,我們加入一個NewsArticle
類別:
public class NewsArticle {
String name;
String link;
public NewsArticle(String name, String link) {
this.name = name;
this.link = link;
}
// Usual getters and setters
}
另外,我們需要一個Reporter
類別:
public class Reporter {
String name;
NewsArticle latestArticle;
public Reporter(String name, NewsArticle latestArticle) {
this.name = name;
this.latestArticle = latestArticle;
}
// Usual getters and setters
}
最後,讓我們建立一個NewsAgency
類別:
public class NewsAgency {
List<Reporter> reporters;
public NewsAgency(List<Reporter> reporters) {
this.reporters = reporters;
}
public List<String> getLatestArticlesNames(){
List<String> results = new ArrayList<>();
for(Reporter reporter : this.reporters){
results.add(reporter.getLatestArticle().getName());
}
return results;
}
}
了解它們之間的關係很重要。首先, Reporter
報道NewsArticle
。一名Reporter
在NewsAgency.
NewsAgency
包含一個getLatestArticlesNames()
方法,該方法傳回與 NewsAgency 合作的所有reporters
撰寫的最新文章的名稱NewsAgency.
該方法將接受我們的單元測試。
讓我們透過初始化所有物件來首次嘗試此單元測試。
3. 初始化對象
在我們的測試中,作為第一種方法,我們將初始化所有物件:
public class NewsAgencyTest {
@Test
void getAllArticlesTest(){
String title1 = "new study reveals the dimension where the single socks disappear";
NewsArticle article1 = new NewsArticle(title1,"link1");
Reporter reporter1 = new Reporter("Tom", article1);
String title2 = "secret meeting of cats union against vacuum cleaners";
NewsArticle article2 = new NewsArticle(title2,"link2");
Reporter reporter2 = new Reporter("Maria", article2);
List<String> expectedResults = List.of(title1, title2);
NewsAgency newsAgency = new NewsAgency(List.of(reporter1, reporter2));
List<String> actualResults = newsAgency.getLatestArticlesNames();
assertEquals(expectedResults, actualResults);
}
}
我們可以看到,當我們的物件變得越來越複雜時,所有物件的初始化會變得多麼乏味。由於模擬正是達到這個目的,我們將使用它們來簡化和避免繁瑣的初始化。
4. 模擬對象
讓我們使用模擬來測試相同的方法getLatestArticlesNames()
:
@Test
void getAllArticlesTestWithMocks(){
Reporter mockReporter1 = mock(Reporter.class);
String title1 = "cow flying in London, royal guard still did not move";
when(mockReporter1.getLatestArticle().getName()).thenReturn(title1);
Reporter mockReporter2 = mock(Reporter.class);
String title2 = "drunk man accidentally runs for mayor and wins";
when(mockReporter2.getLatestArticle().getName()).thenReturn(title2);
NewsAgency newsAgency = new NewsAgency(List.of(mockReporter1, mockReporter2));
List<String> expectedResults = List.of(title1, title2);
assertEquals(newsAgency.getLatestArticlesNames(), expectedResults);
}
如果我們嘗試按原樣執行此測試,我們將收到空指標異常。根本原因是對mockReporter1.getLastestArticle()
的呼叫傳回null
,這是預期的行為:模擬是物件的無效版本。
5. 使用深存根
深度存根是模擬巢狀呼叫的簡單解決方案。深度存根幫助我們利用模擬並僅存根測試中所需的呼叫。
讓我們在我們的範例中使用它,我們將使用模擬和深度存根重寫單元測試:
@Test
void getAllArticlesTestWithMocksAndDeepStubs(){
Reporter mockReporter1 = mock(Reporter.class, Mockito.RETURNS_DEEP_STUBS);
String title1 = "cow flying in London, royal guard still did not move";
when(mockReporter1.getLatestArticle().getName()).thenReturn(title1);
Reporter mockReporter2 = mock(Reporter.class, Mockito.RETURNS_DEEP_STUBS);
String title2 = "drunk man accidentally runs for mayor and wins";
when(mockReporter2.getLatestArticle().getName()).thenReturn(title2);
NewsAgency newsAgency = new NewsAgency(List.of(mockReporter1, mockReporter2));
List<String> expectedResults = List.of(title1, title2);
assertEquals(newsAgency.getLatestArticlesNames(), expectedResults);
}
新增Mockito.RETURNS_DEEP_STUBS
允許我們存取所有巢狀方法和物件。在我們的程式碼範例中,我們不需要模擬mockReporter1
中的多個層級的物件來存取mockReporter1.getLatestArticle().getName()
。
六、結論
在本文中,我們學習如何使用深度存根來解決 Mockito 的巢狀方法呼叫問題。
我們應該記住,使用它們的必要性通常是違反德米特定律的症狀,德米特定律是物件導向程式設計的準則,有利於低耦合並避免嵌套方法呼叫。因此,應該為遺留程式碼保留深度存根,並且在乾淨的現代程式碼中,我們應該支援重構嵌套呼叫。
範例的完整原始碼可在GitHub 上取得。