单元测试及mock使用深度解析

一、Mockito 原理深度解析

1. 什么是 Mockito?

Mockito 是 Java 领域最流行的 Mock 框架之一,主要用于单元测试中模拟(Mock)对象的行为,隔离被测试代码,便于测试。

2. Mock 的本质

Mockito 的核心是通过动态代理(主要是 cglib 和 JDK Proxy)生成对象的 Mock 实例。Mock 实例可以拦截方法调用,并根据预设的行为返回相应的结果。

  • 对接口类型,使用 JDK 动态代理。
  • 对类类型,使用 cglib 生成子类实现。

3. 主要原理

  • 方法拦截:Mock 对象的方法调用会被拦截,转而执行 Mockito 内部的逻辑。
  • 行为设定:通过 when(...).thenReturn(...) 等语法设定方法调用的返回值或抛出异常。
  • 调用验证:通过 verify(...) 检查方法是否被调用、调用次数等。

4. 运行流程

  1. 创建 Mock 对象(mock(Class) 或注解)。
  2. 设定方法行为(when(mock.method()).thenReturn(value))。
  3. 调用 Mock 方法。
  4. 验证调用(verify(mock).method())。

二、Mockito 所有注解说明

1. @Mock

  • 作用:声明一个 Mock 对象。
  • 原理:在测试运行前,Mockito 根据类型生成 Mock 实例并注入到字段。
@Mock
List<String> mockedList;

2. @Spy

  • 作用:声明一个 Spy(部分 Mock)对象,真实对象的包装,可以拦截部分方法。
  • 原理:创建真实对象,然后用代理包装,未设定的行为走真实方法,设定的走 Mock 行为。
@Spy
List<String> spyList = new ArrayList<>();

3. @InjectMocks

  • 作用:自动将 @Mock 或 @Spy 的字段注入到被测试对象中。
  • 原理:通过反射自动查找构造函数或字段,将 Mock 实例注入。
@InjectMocks
MyService myService;

4. @Captor

  • 作用:声明一个 ArgumentCaptor,用于捕获方法参数。
  • 原理:自动创建 ArgumentCaptor 实例。
@Captor
ArgumentCaptor<String> captor;

5. @RunWith(MockitoJUnitRunner.class)

  • 作用:让 JUnit 测试运行时自动初始化 Mockito 注解。
  • 原理:JUnit Runner 扩展,运行时自动扫描和注入 Mock/Spy 对象。
@RunWith(MockitoJUnitRunner.class)
public class MyTest { ... }

6. @ExtendWith(MockitoExtension.class)(JUnit 5)

  • 作用:JUnit 5 下,类似于 @RunWith,自动处理 Mockito 注解。
  • 原理:JUnit 5 Extension 机制,自动初始化 Mock/Spy。
@ExtendWith(MockitoExtension.class)
class MyTest { ... }

三、使用原理

  1. Mock/Spy 创建:通过注解或 Mockito.mock()/Mockito.spy() 创建代理对象。
  2. 行为设定when(mock.method()).thenReturn(value) 或 doReturn(value).when(spy).method()
  3. 调用验证verify(mock).method(),可指定次数或参数。
  4. 参数捕获ArgumentCaptor 捕获调用参数。
  5. 自动注入@InjectMocks 自动将 Mock 注入到被测对象依赖。

四、示例代码

1. 基本 Mock 示例

import org.junit.Test;
import org.mockito.Mockito;

import java.util.List;

public class MockitoDemo {
    @Test
    public void testMock() {
        List<String> mockList = Mockito.mock(List.class);

        Mockito.when(mockList.get(0)).thenReturn("hello");

        System.out.println(mockList.get(0)); // 输出 hello
        System.out.println(mockList.get(1)); // 输出 null

        Mockito.verify(mockList).get(0); // 验证 get(0) 被调用过
    }
}

2. 注解使用示例

import org.junit.runner.RunWith;
import org.mockito.*;
import org.mockito.junit.MockitoJUnitRunner;

import java.util.List;

@RunWith(MockitoJUnitRunner.class)
public class MockitoAnnotationDemo {

    @Mock
    List<String> mockList;

    @Spy
    List<String> spyList = new java.util.ArrayList<>();

    @Captor
    ArgumentCaptor<String> captor;

    @InjectMocks
    MyService myService;

    @Test
    public void testAnnotations() {
        Mockito.when(mockList.get(0)).thenReturn("mocked");

        spyList.add("real");
        Mockito.doReturn("spy-mocked").when(spyList).get(0);

        myService.doSomething();

        Mockito.verify(mockList).get(0);
        Mockito.verify(spyList).get(0);

        Mockito.verify(myService).doSomething();
    }
}

class MyService {
    @Mock
    List<String> list;

    public void doSomething() {
        list.get(0);
    }
}

五、常用 API 速查

  • Mockito.mock(Class) 创建 Mock。
  • Mockito.spy(Object) 创建 Spy。
  • Mockito.when(...).thenReturn(...) 设定行为。
  • Mockito.verify(mock).method() 验证调用。
  • ArgumentCaptor<T> 捕获参数。
  • MockitoAnnotations.openMocks(this) 手动初始化注解(无需 Runner/Extension 时)。

总结

  1. Mockito 通过动态代理实现 Mock/Spy。
  2. 注解简化 Mock/Spy/Captor 的声明和注入。
  3. 行为设定和验证是核心用法。
  4. 推荐结合 JUnit Runner/Extension 自动初始化注解。

六、深度详解

 

1、verify(mock, times(n)).method(args);

1.1. 基本原理

verify 是 Mockito 的验证机制,用于断言某个 mock 对象的某个方法,在测试期间被调用了多少次,并且可以验证调用时的参数是否正确。

它不会影响被测代码的执行,只用于测试断言。


1.2. 参数说明

verify(mock, times(n)).method(args);
  • mock:你要验证的 mock 对象(通过 mock() 创建)。
  • times(n):指定期望被调用的次数。常见的有:
    • times(1):正好调用一次
    • times(0):未被调用
    • times(n):调用 n 次
  • method(args):你要验证的方法名和参数。参数可以是具体值,也可以用参数匹配器(如 any()eq() 等)。

1.3. 用途

  • 验证某方法是否被调用,调用次数是否正确。
  • 验证调用时参数是否符合预期。
  • 辅助发现业务逻辑中的遗漏调用或多余调用等问题。

1.4. 常见变体

  • verify(mock).method(args);
    等价于 verify(mock, times(1)).method(args);,即正好被调用一次。
  • verify(mock, never()).method(args);
    验证方法从未被调用
  • verify(mock, atLeastOnce()).method(args);
    至少被调用一次。
  • verify(mock, atLeast(n)).method(args);
    至少被调用 n 次。
  • verify(mock, atMost(n)).method(args);
    最多被调用 n 次。

1.5. 参数匹配器

  • any():匹配任何参数
  • eq(value):匹配等于 value 的参数
  • isNull():匹配 null 参数
  • argThat(...):自定义参数匹配

示例:

verify(mock).method(any());
verify(mock).method(eq("hello"));

1.6. 代码示例

示例 1:验证调用次数和参数

import static org.mockito.Mockito.*;

public class MockitoVerifyDemo {
    public interface DemoService {
        void doSomething(String param);
    }

    public static void main(String[] args) {
        DemoService mockService = mock(DemoService.class);

        // 调用方法
        mockService.doSomething("A");
        mockService.doSomething("B");

        // 验证 doSomething("A") 被调用一次
        verify(mockService, times(1)).doSomething("A");

        // 验证 doSomething("B") 被调用一次
        verify(mockService, times(1)).doSomething("B");

        // 验证 doSomething("C") 未被调用
        verify(mockService, times(0)).doSomething("C");
    }
}

示例 2:参数匹配器

verify(mockService, times(2)).doSomething(anyString());

表示 doSomething 方法被调用了两次,参数是任意字符串。


1.7. 注意事项

  • verify 只能用于 mock 对象,不能用于真实对象。
  • 如果调用次数不符,测试会失败并抛出异常。
  • 如果参数不匹配,测试也会失败。

1.8. 总结

  • verify(mock, times(n)).method(args); 是 Mockito 验证 mock 行为的核心语法。
  • 用于断言方法的调用次数和参数,保证代码逻辑正确。
  • 支持丰富的调用次数和参数匹配方式。

2、Mockito.spy(Object) 参数详解

2.1方法签名

public static <T> T spy(T object)

参数说明

  • object:传入的真实对象实例(非 null),用于创建该对象的“部分模拟(Spy)对象”。
    • 这个对象可以是任意类型的普通 Java 对象(如 List、业务类等),不是接口类型。
    • 不能为 null,否则会抛出异常。

返回值

  • 返回一个“Spy对象”,它是原对象的代理(包装),可以拦截部分方法调用。

用途和原理

用途

  • 用于部分模拟(Partial Mocking):Spy对象既能调用真实对象的方法,也能对部分方法进行行为设定(即 Mock)。
  • 适合场景:你希望在测试中对某些方法进行定制,而其他方法保持真实逻辑。

原理

  • Mockito 会对传入的对象创建一个代理(通常是通过 CGLIB),拦截方法调用。
  • 未设定的行为,调用真实对象方法。
  • 设定的行为(如 doReturn(xxx).when(spy).method(...)),则返回设定值。

使用案例

2.2. 对集合类进行 Spy

import static org.mockito.Mockito.*;

import java.util.ArrayList;
import java.util.List;

public class SpyDemo {
    public static void main(String[] args) {
        List<String> realList = new ArrayList<>();
        List<String> spyList = spy(realList); // 创建 spy

        spyList.add("A"); // 调用真实方法
        spyList.add("B");

        System.out.println(spyList.size()); // 输出 2

        // 对 size() 方法进行 Mock
        when(spyList.size()).thenReturn(10);

        System.out.println(spyList.size()); // 输出 10

        // 其他方法仍然调用真实逻辑
        System.out.println(spyList.get(0)); // 输出 "A"
    }
}

2.3. 对业务类进行 Spy

public class MyService {
    public int add(int a, int b) {
        return a + b;
    }
    public int multiply(int a, int b) {
        return a * b;
    }
}

import static org.mockito.Mockito.*;

public class SpyBusinessDemo {
    public static void main(String[] args) {
        MyService realService = new MyService();
        MyService spyService = spy(realService);

        // 未 mock 方法,调用真实逻辑
        System.out.println(spyService.add(2, 3)); // 输出 5

        // mock multiply 方法
        doReturn(100).when(spyService).multiply(2, 3);

        System.out.println(spyService.multiply(2, 3)); // 输出 100
        System.out.println(spyService.add(5, 5));      // 输出 10(真实方法)
    }
}

2.4. 注意事项

  • 不要用 when(spy.method()).thenReturn(xxx) 方式 mock Spy 的非 void 方法(会实际调用一次真实方法),推荐用 doReturn(xxx).when(spy).method(...)
  • Spy 适合原对象有复杂逻辑,只想定制部分方法时使用。

总结

  • Mockito.spy(Object) 的参数是一个真实对象实例。
  • 返回一个“部分模拟对象”,未设定的方法走真实逻辑,设定的方法走 mock 行为。
  • 推荐用 doReturn().when(spy) 方式定制方法。
  • 适合需要部分 mock 的场景。

3、ArgumentCaptor 作用

ArgumentCaptor 是 Mockito 提供的一个工具类,用于捕获被 mock/stub 方法调用时传入的参数,以便在测试中进行断言和验证。

适用于:

  • 方法参数复杂或动态生成,无法直接断言时。
  • 需要断言方法调用时传递的参数内容。

3.1、参数与泛型

  • ArgumentCaptor<T> 是一个泛型类,T 表示你要捕获的参数类型。
  • 你可以捕获任意类型参数,包括基本类型、对象、集合等。

3.2、使用原理

  1. 创建 ArgumentCaptor 对象(如 ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class))。
  2. 在 verify 或 when 时通过 captor 捕获参数。
  3. 通过 captor 的 getValue() 或 getAllValues() 获取参数值,进行断言。

3.3、典型使用场景

  • 验证 mock 方法调用时,参数是否正确。
  • 捕获多次调用的参数。
  • 复杂对象参数的内容断言。

3.4、代码示例

 基本用法

import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
import org.mockito.ArgumentCaptor;
import org.junit.Test;

public class ArgumentCaptorDemo {

    public interface Service {
        void send(String msg);
    }

    @Test
    public void testArgumentCaptor() {
        Service mockService = mock(Service.class);

        // 调用方法
        mockService.send("hello");

        // 创建 captor
        ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);

        // 验证并捕获参数
        verify(mockService).send(captor.capture());

        // 获取捕获的参数
        String captured = captor.getValue();
        assertEquals("hello", captured);
    }
}

 捕获多个参数

@Test
public void testMultipleArguments() {
    Service mockService = mock(Service.class);

    mockService.send("first");
    mockService.send("second");

    ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);

    verify(mockService, times(2)).send(captor.capture());

    // 获取所有参数
    List<String> allArgs = captor.getAllValues();
    assertEquals(Arrays.asList("first", "second"), allArgs);
}

 捕获复杂对象参数

public class User {
    String name;
    int age;
    // 构造器、getter、setter略
}

public interface UserService {
    void addUser(User user);
}

@Test
public void testComplexArgument() {
    UserService mockUserService = mock(UserService.class);

    mockUserService.addUser(new User("Alice", 20));

    ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
    verify(mockUserService).addUser(captor.capture());

    User capturedUser = captor.getValue();
    assertEquals("Alice", capturedUser.getName());
    assertEquals(20, capturedUser.getAge());
}

注解方式(推荐在 JUnit Runner/Extension下)

@RunWith(MockitoJUnitRunner.class)
public class AnnotationCaptorDemo {
    @Mock
    Service service;

    @Captor
    ArgumentCaptor<String> stringCaptor;

    @Test
    public void testCaptorAnnotation() {
        service.send("hi");
        verify(service).send(stringCaptor.capture());
        assertEquals("hi", stringCaptor.getValue());
    }
}

3.5、常用方法

  • capture():在 verify 或 when 时用来捕获参数。
  • getValue():获取最近一次捕获的参数值。
  • getAllValues():获取所有捕获的参数值(适用于多次调用)。

3.6、注意事项

  • ArgumentCaptor 必须用于 mock 对象的方法(不能用于真实对象)。
  • 捕获参数时,verify 必须与 captor.capture() 配合。
  • 可以捕获多个参数和复杂类型参数。

3.7、总结

  • ArgumentCaptor<T> 用于捕获并断言 mock 方法调用时的参数。
  • 支持单参数、多参数、复杂对象参数。
  • 断言参数内容,提升测试覆盖率和准确性。

 

4、MockitoAnnotations.openMocks(this)

4.1、作用

MockitoAnnotations.openMocks(this) 是 Mockito 3.x 及以上版本推荐的注解初始化方法,用于自动扫描并初始化当前类中的 Mockito 注解(如 @Mock@Spy@Captor@InjectMocks),从而让这些字段生效,无需使用 JUnit Runner 或 Extension。


4.2、参数详解

方法签名

public static AutoCloseable openMocks(Object testClass)
  • testClass:通常是测试类的实例(如 this),即需要扫描注解并初始化的对象。
  • 返回值:AutoCloseable,用于自动资源释放(如与 try-with-resources 配合使用)。

4.3、原理

  • 扫描 testClass 的字段,查找 Mockito 注解(@Mock、@Spy、@Captor、@InjectMocks)。
  • 自动创建 mock/spy/captor 实例,并通过反射赋值到对应字段。
  • 支持依赖注入(@InjectMocks),将已初始化的 mock/spy 注入到被测对象。
  • 等价于 JUnit4 的 @RunWith(MockitoJUnitRunner.class) 或 JUnit5 的 @ExtendWith(MockitoExtension.class),但不依赖测试框架 Runner/Extension。

4.4、典型使用场景

  • 测试类中使用 Mockito 注解,但不想或不能用 Runner/Extension。
  • 与 TestNG、普通 Java main 方法等场景结合使用。
  • 需要手动控制 Mockito 注解初始化时机。

4.5、代码示例

1. 基本用法

import org.junit.Before;
import org.junit.Test;
import org.mockito.*;

public class OpenMocksDemo {
    @Mock
    private List<String> mockList;

    @Spy
    private ArrayList<String> spyList;

    @Captor
    private ArgumentCaptor<String> stringCaptor;

    @InjectMocks
    private MyService myService;

    @Before
    public void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    public void testMockInit() {
        Mockito.when(mockList.size()).thenReturn(10);
        spyList.add("spy");
        Mockito.doReturn("mocked").when(spyList).get(0);

        myService.process();

        // 验证和断言
        Mockito.verify(mockList).size();
        Mockito.verify(spyList).add(stringCaptor.capture());
        assertEquals("spy", stringCaptor.getValue());
    }
}

2. 与 try-with-resources 配合(推荐)

这样可以自动关闭资源,避免潜在的线程泄漏。

import org.mockito.MockitoAnnotations;

public class TryWithResourcesDemo {
    @Mock
    private List<String> mockList;

    @Before
    public void setUp() throws Exception {
        try (AutoCloseable mocks = MockitoAnnotations.openMocks(this)) {
            // 测试代码...
        }
    }
}

4.6、注意事项

  1. 必须在测试方法执行前调用(如在 @Before 方法中)。
  2. 只初始化当前对象的字段,不会递归初始化嵌套类或父类字段。
  3. 返回值需关闭:如果用 try-with-resources,需关闭以释放资源。
  4. 与 @RunWith/@ExtendWith 不可混用,否则可能重复初始化。
  5. 适用于 TestNG、普通 Java 测试、Spring Test 等多种场景

4.7、总结

  • MockitoAnnotations.openMocks(this) 用于自动初始化 Mockito 注解字段。
  • 适合不使用 JUnit Runner/Extension 的场景。
  • 支持 try-with-resources 自动资源管理。
  • 推荐在 @Before 方法中调用,确保 mock/spy/captor 等字段初始化。

5、assert使用

JUnit 提供了静态断言方法(org.junit.Assert,JUnit 4;org.junit.jupiter.api.Assertions,JUnit 5),常用参数如下:

5.1. 常用断言方法及参数

 5.1.1、 assertEquals

用途:判断两个值是否相等。

参数

  • expected:期望值
  • actual:实际值
  • message(可选):失败时显示的消息

用法

assertEquals(expected, actual);
assertEquals(message, expected, actual);

示例

assertEquals(5, add(2, 3));
assertEquals("加法结果不正确", 5, add(2, 3));

5.1.2、 assertNotEquals

用途:判断两个值是否不相等。

assertNotEquals(expected, actual);
assertNotEquals(message, expected, actual);

5.1.3、 assertTrue / assertFalse

用途:判断条件是否为 true 或 false。

assertTrue(condition);
assertTrue(message, condition);

assertFalse(condition);
assertFalse(message, condition);

示例

assertTrue(isValid());
assertFalse("验证应失败", isValid());

5.1.4、 assertNull / assertNotNull

用途:判断对象是否为 null 或非 null。

assertNull(object);
assertNull(message, object);

assertNotNull(object);
assertNotNull(message, object);

5.1.5、 assertSame / assertNotSame

用途:判断两个对象引用是否是同一个对象。

assertSame(expected, actual);
assertNotSame(expected, actual);

5.1.6、 assertArrayEquals

用途:判断两个数组内容是否相等。

assertArrayEquals(expectedArray, actualArray);
assertArrayEquals(message, expectedArray, actualArray);

5.1.7、 assertThrows(JUnit 5)

用途:断言代码块抛出了指定异常。

assertThrows(Exception.class, () -> {
    // 可能抛异常的代码
});

2. 代码示例

import static org.junit.Assert.*;
import org.junit.Test;

public class AssertDemo {

    @Test
    public void testAssert() {
        assertEquals(4, 2 + 2);
        assertNotEquals(5, 2 + 2);
        assertTrue(3 > 2);
        assertFalse(2 > 3);
        assertNull(null);
        assertNotNull(new Object());
        assertSame("abc", "abc");
        assertNotSame(new String("abc"), new String("abc"));
        assertArrayEquals(new int[]{1,2,3}, new int[]{1,2,3});
    }
}

JUnit 5 的异常断言示例:

import static org.junit.jupiter.api.Assertions.*;

@Test
void testException() {
    assertThrows(IllegalArgumentException.class, () -> {
        throw new IllegalArgumentException("error");
    });
}

5.2、AssertJ 断言(更灵活、可读性更强)

AssertJ 是第三方断言库,支持链式断言,参数更多,语义更清晰。

常用用法

import static org.assertj.core.api.Assertions.*;

@Test
void testAssertJ() {
    assertThat(2 + 2).isEqualTo(4);
    assertThat("abc").isNotNull().startsWith("a").endsWith("c");
    assertThat(new int[]{1,2,3}).contains(2);
    assertThatThrownBy(() -> { throw new RuntimeException("fail"); })
        .isInstanceOf(RuntimeException.class)
        .hasMessage("fail");
}

5.3、参数与用途总结

方法主要参数用途说明
assertEqualsexpected, actual, message判断相等
assertNotEqualsexpected, actual, message判断不相等
assertTruecondition, message判断为 true
assertFalsecondition, message判断为 false
assertNullobject, message判断为 null
assertNotNullobject, message判断非 null
assertSameexpected, actual, message判断引用相同
assertNotSameexpected, actual, message判断引用不同
assertArrayEqualsexpected, actual, message判断数组内容相同
assertThrowsexceptionClass, lambda判断抛出指定异常

5.4、使用建议

  • 单元测试中,推荐用断言方法判断每个关键结果。
  • JUnit 5 推荐用 assertThrows 断言异常。
  • 复杂对象、集合断言可用 AssertJ 提高可读性。

创作不易,点赞关注,持续更新!!!

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猩火燎猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值