使用Spring测试流程

使用Spring测试流时,您将特定的业务逻辑流(即用户故事)视为单元,这意味着来自不同层的负责特定流的多个类可能会一起进行测试。但是,被测对象可能依赖于其他对象。它可能需要与数据库交互、与邮件服务器通信或与 Web 服务或消息队列通信。所有这些服务在单元测试期间可能不可用,因此应该被模拟。

Spring 的核心特性之一是它的控制反转 (IoC) 设计,它支持在运行时加载类实现。由于实现是在运行时加载的,因此可以选择适合测试的模拟实现。

一方面,您不能模拟所有类依赖项,因为它们可能是您要测试的流程的一部分。另一方面,您仍然希望修改它们的行为以将它们调整到您的特定测试流程。Spring和Mockito的组合允许您使用Spring构造模拟对象。Spring会在任何层和任何点将您的模拟注入到您的流程中,因此您可以测试定义良好的流程。

流量测试

这将测试将新书添加到图书馆以及相关用户接收通知的整个流程。

流程如下:

  1. 将新书存储在数据库中
  2. 将新书通知读者服务
  3. 读者服务将:
    1. 获取所有读者的列表
    2. 构建一个子列表(来自读者列表),其中仅包含要求获得通知的用户
    3. 向所有要求通知的用户发送通知

下图说明了流程:

使用Spring将Java单元测试扩展到组件-RadeBit瑞安全

在上图中,红色的对象和方法代表模拟数据库交互的模拟对象和模拟调用。通知服务也被嘲笑。我们不想要一个实际的通知,当我们调用它时我们的流程就结束了。

然而,主要流程并没有被模拟;图书馆中的新书会通知所有感兴趣的读者。我们的应用程序使用 Spring 的机制来注入实现。为了注入实现,我们需要通知 Spring:

  1. 来指示这些成员。
  2. 一个包含具体实现的容器。在此示例中,我们将为此作业使用 Java 配置文件。

将由 Spring 构造的注释类成员和指示要构造哪个实现的配置文件的组合是我们干预和注入支持我们测试的 Mockito 模拟实现的地方。

为了注入 Mockito 模拟,我们的测试将与 Spring 一起运行,我们需要告诉 Spring 在哪里寻找它的 bean:

@Configuration
public class LibraryTestConfiguration {
    @Bean
    public Library library() {
        return new LibraryImpl();
    }

    @Bean
    public LibraryDal libraryDal() {
        return mock(LibraryDal.class);
    }

    @Bean
    public ReadersService reader() {
        return spy(new ReadersServiceImpl());
    }

    @Bean
    public NotificationService notificationService() {
        return mock(NotificationService.class);
    }
}

笔记:

  1. 图书馆服务是一个真实的对象。
  2. 数据库层完全用 Mockito 模拟。
  3. 我们使用 Mockito 来监视ReadersService。我们想按原样使用它,但仍然模拟使用数据库的方法。
  4. 端点通知服务是一个模拟。当我们验证它被正确调用时,我们的流程结束。

Spring/Mockito 测试

我们的 Spring/Mockito 联合测试验证了以下流程。

假设我们的图书馆有两个读者,Alice 和 Bob,并且只有 Alice 对从图书馆获取通知感兴趣。当我们将一本新书添加到图书馆时,弗朗茨·卡夫卡 (Franz Kafka) 的《审判》 (  The Trial)会通知爱丽丝这本新书,而鲍勃则不会。

我们通过自动连接所有流程参与者来开始测试:

@Autowired
private Library library;
@Autowired
private LibraryDal libraryDal;
@Autowired
private NotificationService notificationService;
@Autowired
private ReadersService readersService;

Spring 将注入我们在 Spring 配置文件中定义的对象——Mockito mocks 和 spies。下一步是准备初步流动条件:

@Before
public void init()
{
// All library's readers:
Collection<ReaderBean> readers = new HashSet<>();
readers.add(new ReaderBean("Alice", "alice@alice.com", true/*needToNotify*/));
readers.add(new ReaderBean("Bob", "bob@bob.com", false/*needToNotify*/));
doReturn(readers).when(readersService).getAllReaders();
// Return book id upon the book name and author
doAnswer(new Answer()
{
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable
{
Object[] arguments = invocationOnMock.getArguments(); 
return "BookId_" + arguments[0].toString() + "_" + arguments[1].toString();
}
}).when(libraryDal).storeBook(anyString(), anyString());
}

现在我们可以执行测试:

// Start new book flow:
library.newBook("The Trial", "Kafka");
// Verify we sent a notification only to Alice but not to Bob:
verify(notificationService, times(1)).sendNotification("alice@alice.com", "New book in library: 'The Trial' written by Kafka");
verify(notificationService, never()).sendNotification("bob@bob.com", "New book in library: 'The Trial' written by Kafka");
// Verify we stored the book in the database and call the readers service:
verify(libraryDal, times(1)).storeBook("The Trial", "Kafka");
verify(readersService, times(1)).newBookAdded("BookId_The Trial_Kafka", "The Trial", "Kafka");

尽管reader服务是流中的内部元素,但Spring注入机制允许我们操纵其行为。我们不仅验证了Alice收到了通知而Bob没有收到(黑盒测试),而且我们还执行了白盒测试并验证了流程中的正确行为:这本书存储在数据库中,并且我们调用了我们的读者服务正确。