
问题剖析:为何测试失败?
在单元测试中,我们经常使用mocking框架(如mockito)来隔离被测代码,确保测试的焦点仅在于当前逻辑。然而,当被测类(thing)的内部方法(blogic)不通过依赖注入,而是直接在内部创建其依赖对象(如thinggrandparent和thingparent)时,测试就可能面临挑战。
原始测试代码中,Thing类的bLogic方法执行流程如下:
- ThingGrandParent tgp = fun(size);:bLogic方法调用了Thing自身的fun方法,而fun方法内部通过new ThingGrandParent(size)创建了一个真实的ThingGrandParent实例。
- ThingParent tp = tgp.GpFun(size);:接着,这个真实的ThingGrandParent实例调用了GpFun方法,同样在其内部通过new ThingParent(y)创建了一个真实的ThingParent实例。
- st = tp.fetchSideThing();:最后,这个真实的ThingParent实例调用了fetchSideThing方法。
问题在于,测试类ThingTest中声明的@Mock ThingParent tp;和@Mock ThingGrandParent tgpp;这些模拟对象,在t.bLogic(4)的执行过程中从未被使用。t的bLogic方法始终操作的是它自己创建的真实对象。因此,对模拟对象tp设置的when(tgpp.GpFun(anyInt())).thenReturn(tp);虽然语法正确,但由于tgpp这个模拟对象从未被t使用,这行代码的设置也就失去了意义。
当assertEquals(4, t.bLogic(4));执行完毕后,由于tp(模拟对象)的fetchSideThing()方法从未被调用,随后的verify(tp, times(1)).fetchSideThing();自然会失败,并抛出Wanted but not invoked的错误。至于InvocationTargetException,它通常发生在通过反射调用方法时,如果方法内部抛出异常,反射机制会将其封装成InvocationTargetException。在本例中,它可能间接指示了bLogic方法内部执行流程与预期不符,导致了后续的问题。
解决方案:正确模拟内部依赖
解决此类问题的核心在于,我们需要让被测对象t在执行bLogic方法时,能够使用我们提供的模拟对象,而不是它自己创建的真实对象。由于Thing类并未提供依赖注入的接口(例如通过构造函数或setter方法传入ThingGrandParent),我们需要利用Mockito的spy功能来模拟Thing对象自身的内部方法调用。
核心策略:模拟被测对象的内部方法调用
- 将Thing对象声明为spy: t = spy(new Thing()); 这一步是正确的。spy允许我们对一个真实对象进行部分模拟,即可以模拟其某些方法,而其他方法则正常执行。
-
模拟t的fun方法: 我们需要让t.bLogic()内部调用的t.fun()方法返回我们预期的模拟对象tgpp,而不是创建一个新的ThingGrandParent。
when(t.fun(anyInt())).thenReturn(tgpp);
通过这行代码,当t.bLogic()内部调用fun(size)时,它将不再创建新的ThingGrandParent,而是直接获得我们预设的模拟对象tgpp。
-
模拟tgpp的GpFun方法: 现在t.bLogic()已经获得了模拟的tgpp对象,我们需要确保tgpp.GpFun(size)调用也能返回我们预设的模拟对象tp。
when(tgpp.GpFun(anyInt())).thenReturn(tp);
这样,t.bLogic()就能获得模拟的tp对象。
-
模拟tp的fetchSideThing方法: 最后,我们需要确保模拟的tp对象在调用fetchSideThing()时,返回一个符合测试期望的SideThing实例。根据assertEquals(4, t.bLogic(4));,我们期望SideThing.getWeight()返回4。
when(tp.fetchSideThing()).thenReturn(SideThing.get(4));
或者,如果SideThing的创建逻辑简单,也可以直接返回一个真实的SideThing实例:when(tp.fetchSideThing()).thenReturn(new SideThing(4));
代码示例:修正后的测试
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.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
// 原始类定义(此处省略,与问题描述中相同)
// ... SideThing, ThingGrandParent, ThingParent, Thing ...
@RunWith(PowerMockRunner.class)
@PowerMockIgnore("javax.management.*")
@PrepareForTest({Thing.class, SideThing.class}) // PowerMock 相关注解,用于处理更复杂的模拟场景,如静态、final、构造函数等
public class ThingTest {
@Mock
ThingParent tp;
@Mock
ThingGrandParent tgpp;
Thing t;
// 使用 MockitoRule 替代废弃的 MockitoAnnotations.initMocks()
@Rule
public MockitoRule rule = MockitoJUnit.rule();
@Before
public void setup() {
// MockitoAnnotations.initMocks(this); // 此方法已废弃,使用 @Rule 替代
t = spy(new Thing()); // t 是一个真实对象,但其方法可以被部分模拟
}
@Test
public void test1() throws Exception {
// 1. 模拟 t.fun() 方法,使其返回我们期望的模拟 tgpp 对象
when(t.fun(anyInt())).thenReturn(tgpp);
// 2. 模拟 tgpp.GpFun() 方法,使其返回我们期望的模拟 tp 对象
when(tgpp.GpFun(anyInt())).thenReturn(tp);
// 3. 模拟 tp.fetchSideThing() 方法,使其返回一个带有预期权重的 SideThing 对象
// 这样,t.bLogic() 最终会得到这个 SideThing,并调用其 getWeight()
when(tp.fetchSideThing()).thenReturn(SideThing.get(4));
// 执行被测方法
assertEquals(4, t.bLogic(4));
// 验证模拟对象的交互
verify(tp, times(1)).fetchSideThing();
verify(tgpp, times(1)).GpFun(anyInt()); // 也可以验证 tgpp 的调用
verify(t, times(1)).fun(anyInt()); // 也可以验证 t 的 fun 方法的调用
}
}Mockito 初始化最佳实践
原始代码中使用了MockitoAnnotations.initMocks(this);,但该方法已被标记为废弃。在现代Mockito和JUnit集成中,推荐使用MockitoRule(对于JUnit 4)或MockitoExtension(对于JUnit 5)来初始化Mock对象。
对于JUnit 4: 在测试类中添加@Rule注解和MockitoJUnit.rule():
import org.junit.Rule;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
public class ThingTest {
@Rule
public MockitoRule rule = MockitoJUnit.rule();
// ... 其他代码 ...
}MockitoRule会自动处理@Mock、@Spy等注解的初始化,无需手动调用initMocks。
对于JUnit 5: 使用@ExtendWith(MockitoExtension.class):
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class ThingTest {
// ... 其他代码 ...
}Spy 与 InjectMocks 的选择
问题中提到了spy和InjectMocks的区别,以及在本例中如何选择。
- @Spy: 用于对一个真实对象进行部分模拟。这意味着该对象的未被模拟的方法会执行其真实逻辑,而被模拟的方法则执行我们定义的行为。当被测对象内部创建其依赖,或者你需要测试一个真实对象的复杂行为但同时隔离其部分依赖时,spy非常有用。在本例中,Thing t = spy(new Thing());是正确的选择,因为它允许我们模拟t的fun()方法,而bLogic()的其他部分则正常执行。
- @InjectMocks: 用于创建一个类的实例,并尝试将所有@Mock或@Spy注解的字段注入到这个实例中。它主要通过构造函数注入、setter方法注入或字段注入的方式工作。InjectMocks适用于被测类通过依赖注入(而不是内部创建)获取其依赖的情况。
在本案例中,Thing类内部直接调用new ThingGrandParent()和new ThingParent()来创建依赖,它并没有可供InjectMocks注入的ThingGrandParent或ThingParent字段。因此,InjectMocks在这里不适用,spy是更合适的选择,因为它允许我们拦截并模拟Thing对象内部对fun()方法的调用。










