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
→ 依赖复杂MapperA
→ MapperA
又依赖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):
- 收集所有带
@InjectMocks
的字段到集合A(bookStorageService
和bookControlService
)- 收集所有可注入候选到集合B(所有Mock和符合条件的Spy)
- 同时标注
@Spy
和@InjectMocks
的字段永不被视为注入候选!结果:Mockito不知道需将
bookControlService
注入bookStorageService
此外,此配置违背单元测试原则——一个测试类同时测试两个类(BookStorageService
和BookControlService
)。
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对象,通过反射注入被测对象。
✅ 操作步骤:
- 手动配置
BookControlService
Spy- 确保Mockito上下文先初始化(避免Mock使用时出现NPE)
- 通过反射将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);
}
此配置下,可同时验证repairService
和bookControlService
的行为。
7. 结论
本文我们:
- 梳理了单元测试核心概念
- 掌握了
@InjectMocks
、@Spy
、@Mock
在复杂多级注入中的协作 - 学会了手动配置Spy对象并注入被测对象
⚠️ 最终建议:频繁使用多级注入通常是代码设计问题的信号,优先考虑重构或转向集成测试。
完整示例代码请参考:GitHub仓库