商城首页欢迎来到中国正版软件门户

您的位置:首页 >Mockito验证失败与内部依赖模拟解析

Mockito验证失败与内部依赖模拟解析

  发布于2025-09-21 阅读(0)

扫一扫,手机访问

Java单元测试中Mockito Verify失败与内部依赖模拟解析

本文深入探讨了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失败。

原始测试代码的问题在于:

  1. Thing t = spy(new Thing()); 创建了一个Thing的间谍对象。
  2. when(tgpp.GpFun(anyInt())).thenReturn(tp); 设定了tgpp模拟对象的行为。
  3. 当t.bLogic(4)被调用时,它会执行ThingGrandParent tgp = fun(size);。这里的fun(size)是Thing类自己的方法,它会执行return new ThingGrandParent(size);,从而创建了一个全新的ThingGrandParent实例,而不是我们期望的tgpp模拟对象。
  4. 因此,后续的操作都是在这个真实创建的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对象仍然是一个真实对象,其
本文转载于:互联网 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注