Mockito 原理与实战

发布于:2025-09-10 ⋅ 阅读:(18) ⋅ 点赞:(0)

一、Mockito 核心原理

1. 动态代理与字节码生成

Mockito 并非使用标准的 Java 动态代理(java.lang.reflect.Proxy),而是基于 CGLIBASM 库,在运行时:

  • 动态生成目标类的子类(对于非 final 类)
  • 或接口的实现类(对于接口)
  • 重写所有方法,替换为“可控制”的行为

✅ 优势:可以 mock 普通类、抽象类、接口,甚至部分方法(spy


2. Mock 的创建过程

UserRepository mockRepo = mock(UserRepository.class);

执行过程:

  1. 生成子类字节码:CGLIB 创建 UserRepository$$EnhancerByMockito 类
  2. 方法拦截:所有方法调用被重定向到 MockHandler
  3. 行为匹配:根据 when(...).thenReturn(...) 的 stubbing 规则返回值或抛异常
  4. 记录调用:用于后续 verify(...) 验证

3. Stubbing(打桩) vs Verification(验证)

阶段 目的 使用方法
Stubbing 预设方法返回值 when(mock.method()).thenReturn(value)
Verification 验证方法是否被调用 verify(mock).method()

4. Mock vs Spy

类型 行为 适用场景
mock(Class) 全新虚拟对象,所有方法默认返回 null/0/false 完全隔离依赖
spy(Object) 真实对象,但可部分 mock 方法 测试部分逻辑,保留其他真实行为

二、Mockito 核心用法详解

示例:UserServiceTest.java

// 引入 JUnit 5 的测试类
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
// 引入 Mockito 注解和核心类
import org.mockito.*;
// 引入断言库(AssertJ)
import static org.assertj.core.api.Assertions.*;
// 引入 Mockito 的 BDD 风格语法(given/when/then)
import static org.mockito.BDDMockito.*;

// 使用 JUnit 5 的扩展,自动处理 Mockito 注解
class UserServiceTest {

    // @Mock:创建一个 UserRepository 的 mock 对象
    // 所有方法默认返回 null(对于对象)、0(int)、false(boolean)等
    @Mock
    private UserMapper userMapper;

    // @InjectMocks:创建 UserService 实例,并自动将上面的 @Mock 注入到其字段中
    // 相当于:userService = new UserService(userMapper);
    @InjectMocks
    private UserService userService;

    // @BeforeEach:在每个测试方法执行前运行
    @BeforeEach
    void setUp() {
        // 初始化所有 @Mock 和 @InjectMocks 注解
        // 必须调用,否则 mock 不会生效
        MockitoAnnotations.openMocks(this);
    }

    // 测试:创建用户是否成功
    @Test
    void shouldCreateUserSuccessfully() {
        // Given:准备测试数据和预设行为(Arrange)

        // 创建一个待插入的用户对象
        User user = new User("Alice", "alice@example.com");
        // 设置 ID 为 null,表示尚未保存
        user.setId(1L);

        // 打桩(Stubbing):当调用 userMapper.insert(any(User.class)) 时
        // any(User.class) 表示匹配任意 User 类型的参数
        // thenAnswer 允许我们编写更复杂的逻辑,比如模拟主键回填
        when(userMapper.insert(any(User.class)))
                .thenAnswer(invocation -> {
                    // 获取传入的参数(即要插入的 user 对象)
                    User u = invocation.getArgument(0);
                    // 模拟数据库生成主键的行为,设置 ID = 1
                    u.setId(1L);
                    // 返回 1 表示插入成功(影响行数)
                    return 1;
                });

        // When:执行被测方法(Act)
        // 调用 userService 的 createUser 方法
        User result = userService.createUser("Alice", "alice@example.com");

        // Then:验证结果(Assert)

        // 断言:返回的用户 ID 应该是 1
        assertThat(result.getId()).isEqualTo(1L);

        // 验证:userMapper.insert 方法是否被调用了一次
        // argThat(...) 用于自定义参数匹配器
        // 这里验证传入的 User 对象的 name 字段是 "Alice"
        verify(userMapper).insert(argThat(u -> u.getName().equals("Alice")));
    }

    // 测试:当用户不存在时应抛出异常
    @Test
    void shouldThrowExceptionWhenUserNotFound() {
        // Given:预设行为

        // 当调用 findById(999L) 时返回 null(表示数据库查不到)
        given(userMapper.findById(999L)).willReturn(null);

        // When & Then:执行并验证异常(AssertJ 风格)

        // 断言:调用 getUserById(999L) 会抛出 UserNotFoundException
        assertThatThrownBy(() -> userService.getUserById(999L))
                // 并且异常类型是 UserNotFoundException
                .isInstanceOf(UserNotFoundException.class)
                // 并且异常消息包含 "999"
                .hasMessageContaining("999");
    }

    // 测试:获取所有用户
    @Test
    void shouldReturnAllUsers() {
        // Given:预设 findAll() 返回一个包含一个用户的列表
        User user1 = new User("Bob", "bob@example.com");
        user1.setId(2L);
        // 将预设行为绑定到 mock 对象
        given(userMapper.findAll()).willReturn(java.util.Arrays.asList(user1));

        // When:调用服务方法
        List<User> result = userService.getAllUsers();

        // Then:验证结果
        assertThat(result).hasSize(1);           // 列表大小为 1
        assertThat(result.get(0).getName()).isEqualTo("Bob"); // 第一个用户名字是 Bob
    }

    // 测试:更新用户
    @Test
    void shouldUpdateUserSuccessfully() {
        // Given:预设 findById 返回一个用户,update 返回影响行数 1
        User existingUser = new User("OldName", "old@example.com");
        existingUser.setId(1L);
        given(userMapper.findById(1L)).willReturn(existingUser);
        given(userMapper.update(any(User.class))).willReturn(1);

        // When:调用更新方法
        User updated = userService.updateUser(1L, "NewName", "new@example.com");

        // Then:验证
        assertThat(updated.getName()).isEqualTo("NewName");
        // 验证 update 方法被调用了一次
        verify(userMapper).update(argThat(u ->
                u.getName().equals("NewName") &&
                u.getEmail().equals("new@example.com")
        ));
    }

    // 测试:删除用户
    @Test
    void shouldDeleteUser() {
        // Given:预设 findById 返回用户,deleteById 返回 1
        given(userMapper.findById(1L)).willReturn(new User("ToDelete", "del@example.com"));
        given(userMapper.deleteById(1L)).willReturn(1);

        // When:执行删除
        userService.deleteUser(1L);

        // Then:验证 deleteById 被调用了一次,参数是 1L
        verify(userMapper).deleteById(eq(1L)); // eq(1L) 明确匹配 1L
    }

    // 测试:验证方法调用次数
    @Test
    void shouldVerifyCallCount() {
        // Given
        given(userMapper.findAll()).willReturn(java.util.Collections.emptyList());

        // When
        userService.getAllUsers();
        userService.getAllUsers(); // 调用两次

        // Then:验证 findAll() 被调用了 2 次
        verify(userMapper, times(2)).findAll();

        // 其他调用次数验证:
        // verify(userMapper, never()).deleteById(999L);       // 从未调用
        // verify(userMapper, atLeastOnce()).findAll();       // 至少调用一次
        // verify(userMapper, atMost(3)).findAll();           // 最多调用 3 次
    }

    // 测试:抛出异常
    @Test
    void shouldThrowExceptionOnSave() {
        // Given:当 insert 被调用时抛出 RuntimeException
        doThrow(new RuntimeException("DB Error"))
                .when(userMapper).insert(any(User.class));

        // When & Then
        assertThatThrownBy(() -> userService.createUser("Fail", "fail@example.com"))
                .hasMessageContaining("DB Error");
    }

    // 测试:Spy(部分模拟)
    @Test
    void shouldUseSpy() {
        // 创建一个真实 ArrayList,并用 spy 包装
        List<String> list = spy(new java.util.ArrayList<String>());

        // 预设 get(0) 返回 "mocked"
        when(list.get(0)).thenReturn("mocked");

        // add() 仍执行真实逻辑
        list.add("real");

        // 验证
        assertThat(list.get(0)).isEqualTo("mocked"); // 被 mock
        assertThat(list.get(1)).isEqualTo("real");   // 真实行为
        assertThat(list).hasSize(2);                 // 真实 size
    }
}

三、高级用法与最佳实践

1. 参数匹配器(Argument Matchers)

匹配器 说明
any() 任意对象
anyString() 任意字符串
eq("value") 精确匹配
argThat(x -> x > 5) 自定义条件
isNull() null 值
same(obj) 同一个引用

⚠️ 注意:不能混用具体值和匹配器
when(repo.findById(1L, anyString())) → 错误
when(repo.findById(eq(1L), anyString())) → 正确


2. BDD 风格写法(推荐)

// Given
given(userMapper.findById(1L)).willReturn(user);

// When
User result = userService.getUserById(1L);

// Then
then(userMapper).should().findById(1L);

3. Mockito 扩展(JUnit 5)

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock UserMapper mapper;
    @InjectMocks UserService service;
    // 不需要 @BeforeEach 中的 openMocks()
}

四、常见问题与陷阱

问题 解决方案
NullPointerException 忘记 MockitoAnnotations.openMocks(this);
UnnecessaryStubbing 存在未使用的 when(...)
Wanted but not invoked verify 的方法未被调用
Mockito cannot mock this class 类是 final / 无参构造函数私有

网站公告

今日签到

点亮在社区的每一天
去签到