1. 概述

BDD(行为驱动开发)术语最早由Dan North - 2006年提出。

BDD鼓励使用自然、易读的人类语言编写测试,专注于应用程序的行为。它定义了一种清晰的结构化测试编写方式,遵循三个部分(设定、行动、断言):

  • 给定 一些前置条件(设定)
  • 一个动作发生时(行动)
  • 然后 验证输出(断言)

Mockito库随附了一个名为BDDMockito的类,提供了对BDD友好的API。 这个API允许我们采用更符合BDD风格的方式,在使用*given()进行设定和使用then()*进行断言。

在这篇文章中,我们将解释如何设置基于BDD的Mockito测试。我们还将讨论MockitoBDDMockito API之间的差异,最终聚焦于BDDMockito API。

2. 设置

2.1. Maven依赖

BDD版本的Mockito是mockito-core库的一部分,为了开始,我们只需要添加以下依赖:

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

有关Mockito的最新版本,请查看Maven中央仓库

2.2. 导入

我们的测试可以通过包含以下静态导入来变得更易读:

import static org.mockito.BDDMockito.*;

请注意,BDDMockito扩展了Mockito,因此我们不会错过传统Mockito API提供的任何功能。

3. Mockito与BDDMockito

在Mockito中,传统的模拟通常在设定步骤中使用when(obj).then\*()

随后,我们可以在断言步骤中使用verify()验证与模拟对象的交互。

BDDMockitoMockito的各种方法提供了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进行模拟

让我们尝试测试PhoneBookService,其中我们需要模拟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;
    }
}

BDDMockitoMockito一样,允许我们返回固定或动态值,甚至可以抛出异常。

4.1. 返回固定值

使用BDDMockito,我们可以轻松配置Mockito,使其在调用模拟对象目标方法时始终返回固定结果:

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. 抛出异常

告诉Mockito抛出异常相当直接:

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);

注意,我们将givenwill\*的位置交换了,这是必要的,因为我们在模拟没有返回值的方法。

同时注意,我们使用了像anyeq这样的参数匹配器,以提供更通用的基于条件的模拟方式,而不是依赖于固定值。

5. 总结

在这篇快速教程中,我们讨论了BDDMockito如何将BDD的风格融入到我们的Mockito测试中,并探讨了MockitoBDDMockito之间的一些差异。

如往常一样,源代码可以在GitHub上的com.baeldung.bddmockito测试包中找到。