1. 概述
本教程将演示如何使用Mockito的深度存根(Deep Stubs)功能来模拟嵌套方法调用。想深入了解Mockito测试技巧?可以参考我们的Mockito系列教程。
2. 问题解析
在复杂代码(尤其是遗留代码)中,单元测试时初始化所有依赖对象往往非常困难。我们很容易在测试中引入大量不必要的依赖,而直接模拟这些对象又可能导致空指针异常。
通过代码示例,我们来看看这两种方法的局限性。首先需要几个测试类:
public class NewsArticle {
String name;
String link;
public NewsArticle(String name, String link) {
this.name = name;
this.link = link;
}
// 标准getter/setter方法
}
接着是Reporter
类:
public class Reporter {
String name;
NewsArticle latestArticle;
public Reporter(String name, NewsArticle latestArticle) {
this.name = name;
this.latestArticle = latestArticle;
}
// 标准getter/setter方法
}
最后是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;
}
}
理解类关系很重要:
NewsArticle
由Reporter
报道Reporter
为NewsAgency
工作
NewsAgency
的getLatestArticlesNames()
方法会返回所有记者最新文章的名称,这正是我们要测试的目标。先尝试通过初始化所有对象来编写测试。
3. 对象初始化方案
第一种测试方式是初始化所有对象:
public class NewsAgencyTest {
@Test
void getAllArticlesTest(){
String title1 = "新研究揭秘单身袜失踪的维度空间";
NewsArticle article1 = new NewsArticle(title1,"link1");
Reporter reporter1 = new Reporter("Tom", article1);
String title2 = "猫咪工会秘密会议:对抗吸尘器";
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);
}
}
当对象结构变复杂时,这种初始化方式会变得非常繁琐。既然Mock的存在就是为了解决这个问题,我们接下来用模拟对象来简化测试。
4. 模拟对象方案
使用模拟对象测试相同方法:
@Test
void getAllArticlesTestWithMocks(){
Reporter mockReporter1 = mock(Reporter.class);
String title1 = "伦敦奶牛飞天,皇家卫兵纹丝不动";
when(mockReporter1.getLatestArticle().getName()).thenReturn(title1);
Reporter mockReporter2 = mock(Reporter.class);
String title2 = "醉汉意外参选市长并获胜";
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 = "伦敦奶牛飞天,皇家卫兵纹丝不动";
when(mockReporter1.getLatestArticle().getName()).thenReturn(title1);
Reporter mockReporter2 = mock(Reporter.class, Mockito.RETURNS_DEEP_STUBS);
String title2 = "醉汉意外参选市长并获胜";
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
创建多层模拟对象就能直接调用getLatestArticle().getName()
。
6. 总结
本文介绍了使用深度存根解决Mockito嵌套方法调用问题的方案。
⚠️ 需要注意:深度存根的使用往往意味着违反了迪米特法则(面向对象编程中倡导低耦合、避免嵌套调用的原则)。因此深度存根应主要用于遗留代码,在现代化代码中更推荐通过重构消除嵌套调用。