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;
    }
}

理解类关系很重要:

  • NewsArticleReporter报道
  • ReporterNewsAgency工作

NewsAgencygetLatestArticlesNames()方法会返回所有记者最新文章的名称,这正是我们要测试的目标。先尝试通过初始化所有对象来编写测试。

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嵌套方法调用问题的方案。

⚠️ 需要注意:深度存根的使用往往意味着违反了迪米特法则(面向对象编程中倡导低耦合、避免嵌套调用的原则)。因此深度存根应主要用于遗留代码,在现代化代码中更推荐通过重构消除嵌套调用。


原始标题:Mock Nested Method Calls Using Mockito | Baeldung