首页 > Java > java教程 > 正文

Mockito/PowerMock测试中内部依赖模拟的陷阱与解决方案

花韻仙語
发布: 2025-09-02 12:09:39
原创
834人浏览过

Mockito/PowerMock测试中内部依赖模拟的陷阱与解决方案

本文深入探讨了在Mockito和PowerMock测试中,当被测对象内部创建其依赖时,verify失败和InvocationTargetException的常见问题。通过分析具体案例,我们揭示了测试失败的根本原因在于未正确模拟内部方法调用。教程提供了使用spy和when-thenReturn组合来有效模拟内部依赖的策略,并介绍了Mockito初始化规则的现代用法,以及spy与InjectMocks的关键区别,旨在帮助开发者构建健壮的单元测试。

问题剖析:为何测试失败?

在单元测试中,我们经常使用mocking框架(如mockito)来隔离被测代码,确保测试的焦点仅在于当前逻辑。然而,当被测类(thing)的内部方法(blogic)不通过依赖注入,而是直接在内部创建其依赖对象(如thinggrandparent和thingparent)时,测试就可能面临挑战。

原始测试代码中,Thing类的bLogic方法执行流程如下:

  1. ThingGrandParent tgp = fun(size);:bLogic方法调用了Thing自身的fun方法,而fun方法内部通过new ThingGrandParent(size)创建了一个真实的ThingGrandParent实例。
  2. ThingParent tp = tgp.GpFun(size);:接着,这个真实的ThingGrandParent实例调用了GpFun方法,同样在其内部通过new ThingParent(y)创建了一个真实的ThingParent实例。
  3. 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对象自身的内部方法调用。

核心策略:模拟被测对象的内部方法调用

  1. 将Thing对象声明为spy: t = spy(new Thing()); 这一步是正确的。spy允许我们对一个真实对象进行部分模拟,即可以模拟其某些方法,而其他方法则正常执行。
  2. 模拟t的fun方法: 我们需要让t.bLogic()内部调用的t.fun()方法返回我们预期的模拟对象tgpp,而不是创建一个新的ThingGrandParent。
    when(t.fun(anyInt())).thenReturn(tgpp);
    登录后复制

    通过这行代码,当t.bLogic()内部调用fun(size)时,它将不再创建新的ThingGrandParent,而是直接获得我们预设的模拟对象tgpp。

  3. 模拟tgpp的GpFun方法: 现在t.bLogic()已经获得了模拟的tgpp对象,我们需要确保tgpp.GpFun(size)调用也能返回我们预设的模拟对象tp。
    when(tgpp.GpFun(anyInt())).thenReturn(tp);
    登录后复制

    这样,t.bLogic()就能获得模拟的tp对象。

    百度虚拟主播
    百度虚拟主播

    百度智能云平台的一站式、灵活化的虚拟主播直播解决方案

    百度虚拟主播 36
    查看详情 百度虚拟主播
  4. 模拟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()方法的调用。

注意事项与总结

  1. 依赖注入优先: 最佳实践是设计可测试的代码,即通过构造函数或setter方法进行依赖注入,这样可以直接注入Mock对象,简化测试。当无法重构代码时,spy和PowerMock是强大的工具
  2. PowerMock的使用: PowerMock (@RunWith(PowerMockRunner.class), @PrepareForTest) 通常用于处理Mockito无法直接模拟的场景,例如静态方法、final类/方法、构造函数或私有方法。在本例中,虽然通过spy

以上就是Mockito/PowerMock测试中内部依赖模拟的陷阱与解决方案的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号