Mockito 模拟框架:隔离被测单元

FreeGuideOnline 最新 2026-06-17

Mockito 模拟框架:隔离被测单元

为什么需要模拟框架

在单元测试中,我们希望将测试焦点严格限制在被测单元(通常是一个类的方法)上。但现实中的对象往往依赖数据库连接、网络服务、文件系统或其他复杂组件。如果直接使用真实依赖,测试会变得缓慢、脆弱且难以控制。为了让单元测试真正“单元”化,我们需要一种方式来替换这些外部依赖,创造出可控、可预测的测试环境。这正是 Mockito 擅长的领域。

Mockito 是 Java 生态中最流行的模拟框架。它允许你用简洁的 API 创建模拟对象(Mock),设定这些对象的行为,并验证被测代码与它们之间的互动。通过隔离被测单元,你可以获得:

  • 快速执行:无网络、磁盘 I/O,毫秒级完成测试。
  • 确定性:排除外部服务不稳定造成的随机失败。
  • 精确控制:强制依赖返回指定值或抛出异常,全面覆盖边界情况。
  • 交互验证:确保被测代码以预期的方式调用依赖。

环境准备

Mockito 的使用非常简单,只需添加相应依赖即可。对于 Maven 项目,在 pom.xml 中加入:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.11.0</version>
    <scope>test</scope>
</dependency>

如果使用 Gradle:

testImplementation 'org.mockito:mockito-core:5.11.0'

Mockito 5.x 完全支持 JUnit 5,并与 JUnit 4 兼容。建议搭配 JUnit 5 使用以获得最佳体验。


快速入门:第一个模拟对象

让我们从一个典型的场景开始:UserService 依赖 UserRepository 从数据库获取用户。测试 UserService 时,我们不想连接真实数据库。

// 依赖接口
public interface UserRepository {
    User findById(Long id);
}

// 被测服务
public class UserService {
    private final UserRepository repository;
    
    public UserService(UserRepository repository) {
        this.repository = repository;
    }
    
    public String getUserName(Long id) {
        User user = repository.findById(id);
        return user != null ? user.getName() : "Unknown";
    }
}

测试代码:

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

class UserServiceTest {

    @Test
    void shouldReturnUserName() {
        // 1. 创建模拟对象
        UserRepository mockRepo = mock(UserRepository.class);
        
        // 2. 设定模拟行为:当调用 findById(1L) 时,返回一个预设用户
        User fakeUser = new User(1L, "John");
        when(mockRepo.findById(1L)).thenReturn(fakeUser);
        
        // 注:对于 void 方法,使用 doReturn(...).when(...) 或 doThrow(...)
        
        // 3. 将被测对象与模拟对象绑定
        UserService service = new UserService(mockRepo);
        
        // 4. 执行被测方法
        String name = service.getUserName(1L);
        
        // 5. 验证结果
        assertEquals("John", name);
        
        // 6. 验证交互(可选):确保 findById(1L) 被调用了一次
        verify(mockRepo).findById(1L);
    }
}

这个例子中,mock() 创建了一个 UserRepository 的模拟实例。when(mockRepo.findById(1L)) 定义了一个存根(stub)——告诉 Mockito 当碰到这个调用时应返回什么。随后执行业务逻辑,最后用断言验证返回值,并用 verify() 确认模拟对象上的方法确实被调用了。


核心 API 详解

创建模拟对象

  • mock(Class<T> classToMock):最常用的方式,为类或接口创建模拟对象。
  • @Mock 注解:结合 MockitoExtension(JUnit 5)或 MockitoJUnitRunner(JUnit 4)自动创建。
@ExtendWith(MockitoExtension.class)   // JUnit 5 推荐
class UserServiceTest {
    @Mock
    UserRepository repository;        // 自动注入模拟对象
    
    @InjectMocks
    UserService service;              // 自动将 @Mock 对象注入到该实例中
}

@InjectMocks 会尝试通过构造器、Setter 或字段注入将模拟对象注入到标注的实例中,省去手动创建的代码。

设定行为:when/thenReturn 与 stub 系列

when(...).thenReturn(...) 是最基础的 stub 方法。对于有返回值的方法,它定义调用特定参数时返回什么。

when(repository.findById(1L)).thenReturn(new User(1L, "Alice"));
when(repository.findById(2L)).thenReturn(null);
// 连续调用可返回不同值
when(repository.findById(3L)).thenReturn(new User(3L, "Bob"), new User(3L, "Charlie"));
// 第一个调用返回 Bob,第二个调用返回 Charlie

对于 void 方法,由于语法限制,需使用 doReturn(...).when(mock).voidMethod(...) 形式的存根,或者使用 doThrow() 来模拟异常。

doThrow(new RuntimeException("network error")).when(repository).save(any());

验证交互:verify

验证某个模拟对象的方法是否以预期的参数被调用,以及调用的次数。

verify(mockRepo).findById(1L);              // 至少调用一次(默认)
verify(mockRepo, times(2)).findById(1L);    // 精确调用 2 次
verify(mockRepo, never()).findById(999L);   // 从未调用
verify(mockRepo, atLeastOnce()).findById(1L);
verify(mockRepo, atLeast(2)).findById(1L);
verify(mockRepo, atMost(3)).findById(1L);

验证顺序?可以用 InOrder

InOrder inOrder = inOrder(mockRepo);
inOrder.verify(mockRepo).findById(1L);
inOrder.verify(mockRepo).findById(2L);

参数匹配器

如果不需要精确指定参数值,可以使用参数匹配器(Argument Matchers)来放宽条件。

when(repository.findById(anyLong())).thenReturn(defaultUser);
verify(repository).save(any(User.class));

常用匹配器:any(), anyInt(), anyString(), eq(), startsWith(), contains() 等。关键规则:如果其中一个参数使用了匹配器,所有参数都必须使用匹配器

// 错误:混合使用
when(service.process("real", anyString())).thenReturn(x);  // 抛出异常

// 正确:全部使用匹配器
when(service.process(eq("real"), anyString())).thenReturn(x);

模拟异常

测试失败路径时,让模拟方法抛出异常。

// 对有返回值的方法
when(repository.findById(0L)).thenThrow(new IllegalArgumentException("Invalid ID"));

// 对 void 方法
doThrow(new DataAccessException("DB down")).when(repository).save(any());

高级主题

部分模拟:spy

有时你希望保留对象的真实行为,只修改部分方法。这时使用 spy(间谍)

List<String> list = new ArrayList<>();
List<String> spyList = spy(list);

// 真实行为
spyList.add("one");
System.out.println(spyList.get(0)); // 输出 "one"

// 修改部分行为
when(spyList.size()).thenReturn(100);
System.out.println(spyList.size()); // 输出 100

// 验证调用
verify(spyList).add("one");

注意:对 spy 使用 when(...).thenReturn(...) 会调用真实方法,这可能带来副作用。更安全的方式是使用 doReturn()

doReturn(100).when(spyList).size();

参数捕获器:ArgumentCaptor

当需要断言传递给模拟对象方法的复杂参数时,用 ArgumentCaptor 捕获实际参数。

ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
verify(repository).save(captor.capture());

User savedUser = captor.getValue();
assertEquals("Alice", savedUser.getName());

修改无参方法的返回值

对于已创建好的模拟对象,如果你要修改某个无参方法的默认返回值(无参方法默认返回类型默认值:0、false、null),可以使用 Mockito.when(mock.someMethod())。但如果方法返回基本类型,还可以使用特定的便捷方法:

when(mock.isActive()).thenReturn(true);

模拟静态方法

从 Mockito 3.4 开始,支持模拟静态方法。这需要 mockito-inline 模块(5.x 中已内置)。

try (MockedStatic<ClassName> mocked = mockStatic(ClassName.class)) {
    mocked.when(ClassName::staticMethod).thenReturn("mocked");
    // 执行测试代码
}

静态模拟会修改全局状态,务必在 try-with-resources 中使用,确保测试后恢复原状。

重置模拟对象

虽然不推荐过度重置(可能是测试设计不良的信号),但可以使用 reset(mock) 来清空所有存根和调用记录。


最佳实践与常见陷阱

  1. 只为被测单元创建模拟,不要模拟值对象(DTO、实体)或被测类本身。通常模拟接口或依赖的服务。
  2. 使用 @InjectMocks@Mock 的组合,它让测试代码更干净。但要确保注入路径清晰,推荐使用构造器注入。
  3. 避免过度指定:不要对每个调用都验证,只验证对业务关键的行为。verify 应验证有意义的副作用。
  4. 不要模拟类型边界不清晰的类:例如 Java 标准库的类(List 除外。通常用真实对象更好)。
  5. 始终与 JUnit 5 生命周期结合:使用 @ExtendWith(MockitoExtension.class) 自动处理 @Mock@InjectMocks
  6. 注意存根参数匹配的完整性:如果存根未覆盖真实调用参数,将返回默认值(null/0/false),可能导致测试遗忘或误报。
  7. 静态方法、私有方法、final 类模拟需谨慎:它们往往暗示设计需要改进。Mockito 支持这些,但应作为最后手段。
  8. 清理模拟影响:使用静态模拟或 spy 后,确保测试隔离。JUnit 的 @AfterEachMockitoExtension 会自动处理,但静态模拟需要 try-with-resources。
  9. 使用 BDDMockito 风格:在行为驱动开发(BDD)中,使用 given(...).willReturn(...) 替代 when(...).thenReturn(...),使测试意图更清晰。

总结

Mockito 通过模拟外部依赖,让单元测试真正聚焦于被测单元的逻辑。核心流程清晰:创建模拟 → 定义行为 → 执行被测代码 → 验证结果与交互。随着项目的复杂化,@Mock@InjectMocks、参数匹配器、验证模式和部分模拟等工具将帮助你构建健壮且可维护的测试套件。

记住,Mockito 不仅是工具,更是设计反馈器。如果发现难以模拟,通常意味着代码耦合过高,应当重构。拥抱隔离,让你的测试如同其被测代码一样清晰可靠。