1. 概述

本教程将深入探讨Mockito框架中的核心注解:@InjectMocks@Mock@Spy,重点解析它们在多级注入场景下的协作机制。我们将学习重要的测试概念,掌握如何构建合理的测试配置。

2. 多级注入概念

多级注入是个强大的特性,但误用时会带来风险。在深入实践前,我们先梳理关键理论。

2.1 单元测试核心理念

单元测试的本质是隔离测试单个代码单元。在Java生态中,这通常指测试特定类(如Service、Repository、工具类等)。

测试类时,我们只关注业务逻辑,而非依赖项的行为。处理依赖项(如模拟或验证调用)时,我们通常使用Mockito框架——它专为扩展测试引擎(JUnit、TestNG)设计,能高效构建带多依赖的类单元测试。

2.2 @Spy 机制解析

Spy是Mockito的关键支柱,能有效处理依赖项:

  • Mock:完全存根,调用方法时不执行任何操作,且不会触及真实对象
  • Spy:默认将所有调用委托给真实对象方法,但可按需配置为Mock行为

由于Spy默认行为接近真实对象,必须为其设置必要的依赖项。Mockito会尝试隐式注入依赖,但必要时需手动配置。

2.3 多级注入的风险

Mockito的多级注入指:被测类需要Spy,而该Spy又依赖其他需要注入的Mock,形成嵌套结构。

⚠️ 典型场景示例: 测试ServiceA → 依赖复杂MapperAMapperA又依赖ServiceB
常见做法是将MapperA设为Spy,注入ServiceB作为Mock,但这违背单元测试原则

关键原则:当测试需覆盖多个服务时,应转向集成测试
危险信号:频繁需要多级注入 → 意味着测试方法或代码设计存在问题,需重构

3. 示例场景搭建

为展示Mockito的测试能力,我们构建一个图书管理系统案例。核心是类间的依赖关系:

入口类BookStorageService负责存储图书借阅信息,并通过BookControlService验证图书状态:

public class BookStorageService {
    private BookControlService bookControlService;
    private final List<Book> availableBooks;
}

BookControlService依赖两个服务:

  • StatisticService:计算处理图书数量
  • RepairService:检查图书是否需要维修
public class BookControlService {
    private StatisticService statisticService;
    private RepairService repairService;
}

4. 深入 @InjectMocks 注解

要实现Mockito对象间的注入,@InjectMocks看似最直观,但其能力有限。官方文档强调:Mockito不是依赖注入框架,不擅长处理复杂对象网络注入

⚠️ 重要风险:Mockito不会报告注入失败!当无法注入Mock时,字段保持null,导致测试中频发NullPointerException

@InjectMocks在不同配置下行为迥异,并非所有设置都按预期工作。下面详解其使用特性。

4.1 @InjectMocks 与 @Spy 的无效配置

同时使用多个@InjectMocks注解看似合理:

public class MultipleInjectMockDoestWorkTest {
    @InjectMocks
    private BookStorageService bookStorageService;
    @Spy
    @InjectMocks
    private BookControlService bookControlService;
    @Mock
    private StatisticService statisticService;
}

本意是将statisticService注入bookControlService,再将bookControlService注入bookStorageService但此配置在新版Mockito中会引发NPE

🔍 底层原理(Mockito 5.10.0):

  1. 收集所有带@InjectMocks的字段到集合A(bookStorageServicebookControlService
  2. 收集所有可注入候选到集合B(所有Mock和符合条件的Spy)
  3. 同时标注@Spy@InjectMocks的字段永不被视为注入候选

结果:Mockito不知道需将bookControlService注入bookStorageService

此外,此配置违背单元测试原则——一个测试类同时测试两个类(BookStorageServiceBookControlService)。

4.2 @InjectMocks 与 @Spy 的有效配置

当类中仅有一个@InjectMocks注解时,可与@Spy共存:

@Spy
@InjectMocks
private BookControlService bookControlService;
@Mock
private StatisticService statisticService;
@Spy
private RepairService repairService;

此配置构建出正确的测试层级:

@Test
void whenOneInjectMockWithSpy_thenHierarchySuccessfullyInitialized(){
    Book book = new Book("Some name", "Some author", 355, ZonedDateTime.now());
    bookControlService.returnBook(book);

    Assertions.assertNull(book.getReturnDate());
    Mockito.verify(statisticService).calculateAdded();
    Mockito.verify(repairService).shouldRepair(book);
}

5. 通过手动Spy实现多级注入

解决方案之一:在Mockito初始化前手动创建Spy对象。由于Mockito无法向同时标注@Spy@InjectMocks的字段注入依赖,但可向仅标注@InjectMocks的对象注入(即使它是Spy)。

配置方式1:字段初始化

@InjectMocks
private BookControlService bookControlService = Mockito.spy(BookControlService.class);

配置方式2:@BeforeEach初始化

@BeforeEach
public void openMocks() {
    bookControlService = Mockito.spy(BookControlService.class);
    closeable = MockitoAnnotations.openMocks(this);
}

关键要求:Spy创建必须早于Mockito初始化!

完整配置示例:

@InjectMocks
private BookStorageService bookStorageService;
@InjectMocks
private BookControlService bookControlService;
@Mock
private StatisticService statisticService;
@Mock
private RepairService repairService;

测试成功执行:

@Test
void whenSpyIsManuallyCreated_thenInjectMocksWorks() {
    Book book = new Book("Some name", "Some author", 355);
    bookStorageService.returnBook(book);

    Assertions.assertEquals(1, bookStorageService.getAvailableBooks().size());
    Mockito.verify(bookControlService).returnBook(book);
    Mockito.verify(statisticService).calculateAdded();
    Mockito.verify(repairService).shouldRepair(book);
}

6. 通过反射实现多级注入

另一种处理复杂测试场景的方式:手动创建Spy对象,通过反射注入被测对象

操作步骤

  1. 手动配置BookControlService Spy
  2. 确保Mockito上下文先初始化(避免Mock使用时出现NPE)
  3. 通过反射将Spy注入BookStorageService
@InjectMocks
private BookStorageService bookStorageService;
@Mock
private StatisticService statisticService;
@Mock
private RepairService repairService;
private BookControlService bookControlService;

@BeforeEach
public void openMocks() throws Exception {
    bookControlService = Mockito.spy(new BookControlService(statisticService, repairService));
    injectSpyToTestedMock(bookStorageService, bookControlService);
}

private void injectSpyToTestedMock(BookStorageService bookStorageService, BookControlService bookControlService) 
  throws NoSuchFieldException, IllegalAccessException { 
    Field bookControlServiceField = BookStorageService.class.getDeclaredField("bookControlService"); 
    bookControlServiceField.setAccessible(true); 
    bookControlServiceField.set(bookStorageService, bookControlService); 
}

此配置下,可同时验证repairServicebookControlService的行为。

7. 结论

本文我们:

  • 梳理了单元测试核心概念
  • 掌握了@InjectMocks@Spy@Mock在复杂多级注入中的协作
  • 学会了手动配置Spy对象并注入被测对象

⚠️ 最终建议:频繁使用多级注入通常是代码设计问题的信号,优先考虑重构或转向集成测试。

完整示例代码请参考:GitHub仓库


原始标题:Multiple-Level Mock Injection Into Mockito Spy Objects