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.*;

⚠️ 注意:BDDMockitoMockito 的扩展,因此使用它不会丢失任何传统 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)时,必须将 givenwill* 的位置调换。

同时注意我们使用了参数匹配器(如 any, eq)来更灵活地设置 mock 条件,而不是硬编码具体值。

5. 总结

在这篇快速指南中,我们介绍了 BDDMockito 如何为 Mockito 测试带来 BDD 风格的语法,并通过示例展示了它与传统 Mockito 的区别。

✅ 想要查看完整代码?可以前往 GitHub 项目 eugenp/tutorials 中的 com.baeldung.bddmockito 包中获取。


💡 踩坑提示:虽然 BDDMockito 更语义化,但在团队中使用前最好统一风格,避免混用导致代码风格混乱。


原始标题:Quick Guide to BDDMockito