Mockito验证失败与内部依赖模拟解析
本文深入探讨了Mockito单元测试中verify失败的常见原因,特别是当被测对象在内部创建其依赖时。文章详细介绍了如何利用Mockito的spy功能结合doReturn().when()来模拟这些内部创建的依赖,确保测试的正确性。同时,还涵盖了MockitoAnnotations.initMocks的替代方案、spy与InjectMocks的区别,以及PowerMock的使用场景,旨在提供一套全面的Java单元测试最佳实践。

1. 理解测试失败的根本原因:内部依赖创建
在单元测试中,我们通常希望隔离被测单元(System Under Test, SUT),只关注其自身的逻辑,而将其依赖项替换为模拟对象(Mock)。然而,当被测类(如示例中的Thing类)在执行其业务逻辑时,不是通过构造函数或Setter方法接收依赖,而是在其内部方法中直接创建这些依赖(如Thing的fun()方法内部创建ThingGrandParent,ThingGrandParent的GpFun()方法内部创建ThingParent),就会导致verify失败。
原始测试代码的问题在于:
- Thing t = spy(new Thing()); 创建了一个Thing的间谍对象。
- when(tgpp.GpFun(anyInt())).thenReturn(tp); 设定了tgpp模拟对象的行为。
- 当t.bLogic(4)被调用时,它会执行ThingGrandParent tgp = fun(size);。这里的fun(size)是Thing类自己的方法,它会执行return new ThingGrandParent(size);,从而创建了一个全新的ThingGrandParent实例,而不是我们期望的tgpp模拟对象。
- 因此,后续的操作都是在这个真实创建的ThingGrandParent实例及其子依赖上进行的,我们的tgpp和tp模拟对象从未被实际调用,导致verify(tp, times(1)).fetchSideThing()); 失败,并抛出“Wanted but not invoked”的错误。
简而言之,问题出在依赖的创建方式上:被测对象没有使用我们提供的模拟依赖,而是自行创建了真实的依赖。
2. 使用Mockito Spy和Mock解决内部依赖问题
为了解决上述问题,我们需要确保被测对象Thing在执行其业务逻辑时,能够使用我们预设的模拟对象。由于Thing内部通过fun()方法创建了ThingGrandParent,我们可以对Thing的fun()方法进行模拟,使其返回我们准备好的ThingGrandParent模拟对象。
以下是修正后的测试代码示例:
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
// 示例类定义 (与原文保持一致,此处省略以聚焦测试代码)
// @Getter @Setter public class SideThing { ... }
// public class ThingGrandParent { ... }
// public class ThingParent { ... }
// @Getter @Setter public class Thing { ... }
@RunWith(PowerMockRunner.class) // 如果需要PowerMock的其他高级功能,则保留
@PowerMockIgnore("javax.management.*")
// 准备所有涉及模拟或间谍的类,特别是那些最终的、静态的或构造函数被模拟的类
@PrepareForTest({Thing.class, SideThing.class, ThingGrandParent.class, ThingParent.class})
public class ThingTest {
@Mock
ThingParent mockThingParent; // 模拟ThingParent
@Mock
ThingGrandParent mockThingGrandParent; // 模拟ThingGrandParent
@Mock
SideThing mockSideThing; // 模拟SideThing
Thing spyThing; // Thing的间谍对象
// 使用MockitoRule替代 deprecated 的 MockitoAnnotations.initMocks
@Rule
public MockitoRule rule = MockitoJUnit.rule();
@Before
public void setup() {
// 创建Thing的间谍对象,这样可以调用真实方法,也可以模拟特定方法
spyThing = spy(new Thing());
// 不需要 MockitoAnnotations.initMocks(this);
}
@Test
public void testLogicWithMockedDependencies() throws Exception {
int expectedWeight = 4;
int inputSize = 4;
// 1. 模拟 spyThing.fun() 方法的行为
// 当 spyThing 的 fun() 方法被调用时,返回我们的 mockThingGrandParent 模拟对象。
// 使用 doReturn().when() 对 spy 对象进行方法模拟,这比 when().thenReturn() 更安全。
doReturn(mockThingGrandParent).when(spyThing).fun(anyInt());
// 2. 模拟 mockThingGrandParent.GpFun() 方法的行为
// 当 mockThingGrandParent 的 GpFun() 方法被调用时,返回我们的 mockThingParent 模拟对象。
doReturn(mockThingParent).when(mockThingGrandParent).GpFun(anyInt());
// 3. 模拟 mockThingParent.fetchSideThing() 方法的行为
// 当 mockThingParent 的 fetchSideThing() 方法被调用时,返回我们的 mockSideThing 模拟对象。
doReturn(mockSideThing).when(mockThingParent).fetchSideThing();
// 4. 模拟 mockSideThing.getWeight() 方法的行为
// 当 mockSideThing 的 getWeight() 方法被调用时,返回预期的权重值。
when(mockSideThing.getWeight()).thenReturn(expectedWeight);
// 执行被测方法
int result = spyThing.bLogic(inputSize);
// 验证结果
assertEquals(expectedWeight, result);
// 验证方法调用
// 验证 spyThing 的 fun 方法被调用了一次,参数为 inputSize
verify(spyThing, times(1)).fun(inputSize);
// 验证 mockThingGrandParent 的 GpFun 方法被调用了一次,参数为 inputSize
verify(mockThingGrandParent, times(1)).GpFun(inputSize);
// 验证 mockThingParent 的 fetchSideThing 方法被调用了一次
verify(mockThingParent, times(1)).fetchSideThing();
// 验证 mockSideThing 的 getWeight 方法被调用了一次
verify(mockSideThing, times(1)).getWeight();
}
}代码解析:
- @Mock 声明模拟对象: 我们声明了mockThingParent、mockThingGrandParent和mockSideThing作为模拟对象。
- spy(new Thing()) 创建间谍对象: spyThing是一个真实的Thing对象,但我们可以监控其方法调用,并选择性地模拟其某些方法的行为。
- doReturn(mockThingGrandParent).when(spyThing).fun(anyInt());: 这是解决核心问题的关键。它告诉Mockito:当spyThing对象的fun()方法被调用时,不要执行其真实逻辑(即new ThingGrandParent(size)),而是直接返回我们预设的mockThingGrandParent模拟对象。这样,bLogic方法后续对tgp的操作就都会作用在我们的模拟对象上了。
- 链式模拟: 接着,我们按照业务逻辑的调用顺序,模拟了mockThingGrandParent.GpFun()返回mockThingParent,以及mockThingParent.fetchSideThing()返回mockSideThing,最后模拟mockSideThing.getWeight()返回期望值。
- verify 验证: 在执行完业务逻辑后,我们验证了所有关键方法的调用次数和参数,确保了业务流程的正确性。
3. Mockito注解初始化最佳实践
MockitoAnnotations.initMocks(this); 方法在Mockito 2.x版本后已被标记为弃用。对于JUnit 4和JUnit 5,有更现代和推荐的替代方案:
JUnit 4 (@Rule): 使用MockitoJUnit.rule()结合@Rule注解。这会自动初始化所有带有@Mock、@Spy或@InjectMocks注解的字段。
import org.junit.Rule; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; public class MyTest { @Rule public MockitoRule rule = MockitoJUnit.rule(); @Mock MyDependency mockDependency; // ... }JUnit 5 (@ExtendWith): 使用@ExtendWith(MockitoExtension.class)注解。这是JUnit 5中扩展机制的一部分。
import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) public class MyTest { @Mock MyDependency mockDependency; // ... }在示例代码中,我们采用了JUnit 4的@Rule方式。
4. Spy与InjectMocks的选择与应用场景
@Spy和@InjectMocks都是Mockito中用于处理真实对象与模拟对象结合的注解,但它们的应用场景和目的不同。
- @Spy (或 Mockito.spy()):
- 目的: 对一个真实对象进行部分模拟。这意味着该对象的大部分方法会执行其真实逻辑,但你可以选择性地模拟其中一小部分方法。
- 何时使用: 当你需要测试一个真实对象,但该对象内部调用的某些方法(可能是它自己的私有方法,或它内部创建的依赖对象的方法)行为不稳定、耗时或你希望隔离时。在我们的例子中,Thing是一个真实对象,但我们希望模拟它内部调用的fun()方法。
- 特点: spy对象仍然是一个真实对象,其
Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。
极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。
















