0

0

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

花韻仙語

花韻仙語

发布时间:2025-09-02 12:09:39

|

853人浏览过

|

来源于php中文网

原创

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对象。

    讯飞听见会议
    讯飞听见会议

    科大讯飞推出的AI智能会议系统

    下载
  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

相关专题

更多
软件测试常用工具
软件测试常用工具

软件测试常用工具有Selenium、JUnit、Appium、JMeter、LoadRunner、Postman、TestNG、LoadUI、SoapUI、Cucumber和Robot Framework等等。测试人员可以根据具体的测试需求和技术栈选择适合的工具,提高测试效率和准确性 。

426

2023.10.13

java测试工具有哪些
java测试工具有哪些

java测试工具有JUnit、TestNG、Mockito、Selenium、Apache JMeter和Cucumber。php还给大家带来了java有关的教程,欢迎大家前来学习阅读,希望对大家能有所帮助。

295

2023.10.23

Java 单元测试
Java 单元测试

本专题聚焦 Java 在软件测试与持续集成流程中的实战应用,系统讲解 JUnit 单元测试框架、Mock 数据、集成测试、代码覆盖率分析、Maven 测试配置、CI/CD 流水线搭建(Jenkins、GitHub Actions)等关键内容。通过实战案例(如企业级项目自动化测试、持续交付流程搭建),帮助学习者掌握 Java 项目质量保障与自动化交付的完整体系。

19

2025.10.24

硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

989

2023.10.19

PHP接口编写教程
PHP接口编写教程

本专题整合了PHP接口编写教程,阅读专题下面的文章了解更多详细内容。

49

2025.10.17

php8.4实现接口限流的教程
php8.4实现接口限流的教程

PHP8.4本身不内置限流功能,需借助Redis(令牌桶)或Swoole(漏桶)实现;文件锁因I/O瓶颈、无跨机共享、秒级精度等缺陷不适用高并发场景。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

193

2025.12.29

class在c语言中的意思
class在c语言中的意思

在C语言中,"class" 是一个关键字,用于定义一个类。想了解更多class的相关内容,可以阅读本专题下面的文章。

455

2024.01.03

python中class的含义
python中class的含义

本专题整合了python中class的相关内容,阅读专题下面的文章了解更多详细内容。

6

2025.12.06

桌面文件位置介绍
桌面文件位置介绍

本专题整合了桌面文件相关教程,阅读专题下面的文章了解更多内容。

0

2025.12.30

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 2.1万人学习

C# 教程
C# 教程

共94课时 | 5.6万人学习

Java 教程
Java 教程

共578课时 | 39.6万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

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