1. 概述
行为驱动开发(BDD)这个概念最早由 Dan North 在 2006 年提出。
BDD 鼓励我们以一种自然、人类可读的方式编写测试,聚焦于应用程序的行为。
它定义了一种结构清晰的测试写法,通常分为三个部分(Arrange、Act、Assert):
- given 一些前置条件(Arrange)
- when 执行某个动作(Act)
- then 验证输出结果(Assert)
✅ Mockito 框架内置了 BDDMockito 类,提供了更贴近 BDD 风格的 API。 这个 API 让我们可以通过 given() 来安排测试前置条件,用 then() 来做断言。
本文将讲解如何搭建基于 BDD 风格的 Mockito 测试环境,并对比传统 Mockito 与 BDDMockito 的差异,最后聚焦在 BDDMockito 的使用方式上。
2. 环境准备
2.1. Maven 依赖
BDD 风格的 Mockito 已经包含在 mockito-core 库中,因此我们只需要引入如下依赖即可:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.11.0</version>
</dependency>
如需最新版本,可以前往 Maven Central 查看。
2.2. 静态导入
为了让测试代码更简洁可读,建议添加以下静态导入语句:
import static org.mockito.BDDMockito.*;
⚠️ 注意:BDDMockito 是 Mockito 的扩展,因此使用它不会丢失任何传统 Mockito 的功能。
3. Mockito 与 BDDMockito 对比
传统 Mockito 中,我们在 Arrange 阶段使用 when(obj).then*() 来配置 mock 行为。
然后在 Assert 阶段使用 verify() 来验证 mock 的调用情况。
✅ *而 BDDMockito 提供了 BDD 风格的别名方法,允许我们使用 given(代替 when)来安排前置条件,用 then(代替 verify)来做断言。*
来看一个传统 Mockito 的测试示例:
when(phoneBookRepository.contains(momContactName))
.thenReturn(false);
phoneBookService.register(momContactName, momPhoneNumber);
verify(phoneBookRepository)
.insert(momContactName, momPhoneNumber);
再看等效的 BDDMockito 写法:
given(phoneBookRepository.contains(momContactName))
.willReturn(false);
phoneBookService.register(momContactName, momPhoneNumber);
then(phoneBookRepository)
.should()
.insert(momContactName, momPhoneNumber);
是不是更语义清晰?✅
4. 使用 BDDMockito 进行 Mock
我们来测试一个 PhoneBookService 类,其中需要 mock 一个 PhoneBookRepository:
public class PhoneBookService {
private PhoneBookRepository phoneBookRepository;
public void register(String name, String phone) {
if(!name.isEmpty() && !phone.isEmpty()
&& !phoneBookRepository.contains(name)) {
phoneBookRepository.insert(name, phone);
}
}
public String search(String name) {
if(!name.isEmpty() && phoneBookRepository.contains(name)) {
return phoneBookRepository.getPhoneNumberByContactName(name);
}
return null;
}
}
和 Mockito 一样,BDDMockito 支持返回固定值、动态值,也可以抛出异常:
4.1. 返回固定值
使用 BDDMockito 可以轻松地让 mock 方法在被调用时返回一个固定值:
given(phoneBookRepository.contains(momContactName))
.willReturn(false);
phoneBookService.register(xContactName, "");
then(phoneBookRepository)
.should(never())
.insert(momContactName, momPhoneNumber);
4.2. 返回动态值
BDDMockito 还支持根据输入参数动态返回值:
given(phoneBookRepository.contains(momContactName))
.willReturn(true);
given(phoneBookRepository.getPhoneNumberByContactName(momContactName))
.will((InvocationOnMock invocation) ->
invocation.getArgument(0).equals(momContactName)
? momPhoneNumber
: null);
phoneBookService.search(momContactName);
then(phoneBookRepository)
.should()
.getPhoneNumberByContactName(momContactName);
4.3. 抛出异常
让 mock 方法抛出异常也非常简单:
given(phoneBookRepository.contains(xContactName))
.willReturn(false);
willThrow(new RuntimeException())
.given(phoneBookRepository)
.insert(any(String.class), eq(tooLongPhoneNumber));
try {
phoneBookService.register(xContactName, tooLongPhoneNumber);
fail("Should throw exception");
} catch (RuntimeException ex) { }
then(phoneBookRepository)
.should(never())
.insert(momContactName, tooLongPhoneNumber);
⚠️ 注意:当我们 mock 的是一个无返回值的方法(void)时,必须将 given 和 will* 的位置调换。
同时注意我们使用了参数匹配器(如 any, eq)来更灵活地设置 mock 条件,而不是硬编码具体值。
5. 总结
在这篇快速指南中,我们介绍了 BDDMockito 如何为 Mockito 测试带来 BDD 风格的语法,并通过示例展示了它与传统 Mockito 的区别。
✅ 想要查看完整代码?可以前往 GitHub 项目 eugenp/tutorials 中的 com.baeldung.bddmockito 包中获取。
💡 踩坑提示:虽然 BDDMockito 更语义化,但在团队中使用前最好统一风格,避免混用导致代码风格混乱。