Skip to content

21测试方案:如何验证响应式编程组件的正确性?

作为整个课程最后一部分内容,从这一讲开始,我们将讨论响应式 Spring 所提供的测试解决方案。

对于响应式系统而言,测试是一个难点。当一个应用程序中涉及数据层、服务层、Web 层以及各种外部服务之间的交互关系时,除了针对各层组件的独立测试之外,还需要充分引入集成测试来保证服务的正确性和稳定性。

这一讲,我将帮助你梳理全栈响应式测试方案,并给出 Spring 中所提供的相关测试组件。

全栈响应式测试方案

在一个 Web 应用程序中,涉及测试的维度有很多,包括数据访问、服务构建和服务集成等。同时,基于常见的系统分层和代码组织结构,测试工作也体现为一种层次关系,即我们需要测试从 Repository 层到 Service 层、再到 Controller 层的完整业务链路。

而在响应式 Web 应用中,因为其推崇的是全栈式的响应式编程模型,所以每一层都需要对响应式组件进行测试。针对这一点,我梳理了如下所示的测试层次关系,并给出了各个层次中所使用到的主要测试实现方法。

响应式 Web 应用程序测试的层次和方法

不同层次的测试方法需要使用不同的测试工具和框架,我们将关注响应式 Web 应用程序中开展多维度测试的各种工具。此外,传统的以 JUnit 为代表的单元测试工具,以及各种 Mock 框架同样适用于响应式系统的测试。同时,我们也需要采用如上图中所展示的一些特有的测试工具类以及专用的测试注解。

下面就来说说如何测试响应式流。

测试响应式数据流

在 Spring 中,全栈式的响应式编程模型的背后都是基于 Reactor 框架构建的,因此我们首先需要确保数据流运行的正确性,这是开展响应式测试工作的基础。为此,Reactor 框架为我们提供了专门的 reactor-test 测试组件,在 pom 中引入该组件的方式如下所示。

xml
<dependency>
        <groupId>io.projectreactor</groupId>
        <artifactId>reactor-test</artifactId>
        <scope>test</scope>
</dependency>

reactor-test 测试组件中的核心类是 StepVerifier,使用 StepVerifier 的示例代码如下所示。

java
@Test
public void testStepVerifier() {
        Flux<String> helloWorld = Flux.just("Hello", "World");
 
        StepVerifier.create(helloWorld)
           .expectNext("Hello")
           .expectNext("World")
           .expectComplete()
           .verify();
}

上述代码展示了 StepVerifier 类的使用方法,包括如下几个步骤。

  • 初始化:首先将已有的 Mono 或 Flux 数据流对象传入 StepVerifier 的 create() 方法。

  • 设置正常数据流断言:多次调用 expectNext()、expectNextMatches() 方法设置断言,验证数据流对象每一步产生的数据是否符合预期。

  • 设置完成数据流断言:调用 expectComplete() 方法设置断言,验证数据流是否满足正常结束的预期。

  • 设置异常数据流断言:调用 expectError() 方法设置断言,验证数据流是否满足异常结束的预期。

  • 启动测试:调用 verify() 方法启动测试。

上述示例使用了 expectNext() 方法来验证数据流中的元素,下面同样来看一个使用 expectNextMatches() 方法的示例。

java
@Test
public void testExpectNextMatches() {
	StepVerifier
      	.create(Flux.just("jian-hu", "xiang-hu"))
      	.expectSubscription()
        .expectNextMatches(e -> e.startsWith("jian"))
      	.expectNextMatches(e -> e.startsWith("xiang"))
      	.expectComplete()
      	.verify();
}

expectNextMatches() 和 expectNext() 之间的唯一区别是,前者可以实现自定义的条件匹配器 Predicate,使用上比后者更灵活。

类似地,assertNext() 使得编写自定义断言成为可能,如下面的代码所示。

java
@Test
public void testAssertNext() {
        Account testAccount = new Account("1", "accountCode1", "accountName1");
 
        StepVerifier
           .create(Flux.just(testAccount))
           .expectSubscription()
           .assertNext(
               account -> account.getAccountCode().equals("accountCode1")
           )
           .expectComplete()
           .verify();
}

显然,上述示例展示了正常场景下的测试方法。针对业务处理失败、网络访问中断等异常场景,我们也可以设计如下所示的测试用例。

java
@Test
public void testStepVerifierWithError() {
        Flux<String> helloWorldWithException 
           = helloWorld.concatWith(Mono.error(new IllegalArgumentException("exception")));
 
        StepVerifier.create(helloWorldWithException)
           .expectNext("Hello")
           .expectNext("World")
           .expectErrorMessage("exception")
           .verify();
}

这里,我们使用了 Reactor 框架所提供的 concatWith 操作符,拼接了一个正常的 Flux 对象和一个代表系统异常的 Mono 对象,然后借助 StepVerifier 提供的 expectErrorMessage() 方法对抛出的异常消息进行验证。

现在,我们已经可以对响应式流进行测试了。接下来,让我们把讨论范围进行扩大,来研究贯穿整个应用程序的全栈式的响应式测试方案。

测试 Spring Boot 应用程序

在本专栏中,我们统一使用 Spring Boot 来开发响应式的 Web 应用程序,而 Spring Boot 本身也提供了一套完整的测试解决方案。

Spring Boot 中的测试解决方案

和 Spring Boot 1.x 版本一样,Spring Boot 2.x 同样提供了针对测试的 spring-boot-starter-test 组件,我们首先在 pom 文件中添加如下依赖。

java
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
</dependency>

然后我们通过 Maven 查看 spring-boot-starter-test 组件的依赖关系会发现,一系列组件被自动引入到了代码工程的构建路径中,包括 JUnit、JSON Path、AssertJ、Mockito、Hamcrest 等。这里有必要对其中常用的部分组件展开介绍。

  • JUnit 是一款非常流行的基于 Java 语言的单元测试框架,我们的课程中会使用该框架作为基础的测试框架。

  • AssertJ 是强大的流式断言工具,它遵守 3A 核心原则,即 Arrange(初始化测试对象或者准备测试数据)-> Actor(调用被测方法)-> Assert(执行断言)。

  • Mockito 是 Java 世界中一款流行的 Mock 测试框架,使用简洁的 API 实现模拟操作。在实施集成测试时,我们会大量使用这个框架。

以上组件的依赖关系是自动导入的,一般不需要做任何变动。而对于某些特定场景而言,我们可能还需要手工导入一些组件以满足测试需求,例如针对测试场景的嵌入式 MongoDB 数据库 Flapdoodle,我们将在下一讲讨论它的使用方式。

接下来,我们将初始化 Spring Boot 应用程序的测试环境,并向你介绍在单个服务内部完成单元测试的方法和技巧。在导入 spring-boot-starter-test 依赖之后,我们就可以使用它所提供的各项功能来应对复杂的测试场景了。

初始化测试环境

我们知道对于 Spring Boot 应用程序而言,在 Bootstrap 类中将通过 SpringApplication.run() 方法来启动 Spring 容器。如下所示的就是一个典型的 Spring Boot 启动类 CustomerApplication。

java
@SpringBootApplication
public class CustomerApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(CustomerApplication.class, args);
        }
}

基于 Maven 的默认风格,我们将在 src/test/java 和 src/test/resources 包下添加各种测试用例代码和配置文件。针对上述 Bootstrap 类,我们可以通过如下所示的测试用例来验证 Spring 容器是否能够正常启动。


java
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.junit4.SpringRunner;
 
@SpringBootTest
@RunWith(SpringRunner.class)
public class ApplicationContextTests {
 
    @Autowired
    private ApplicationContext applicationContext;
 
    @Test
    public void testContextLoads() throws Throwable {
        Assert.assertNotNull(this.applicationContext);
    }
}

可以看到该代码中的 testContextLoaded() 即为一个有效的测试用例,该用例很简单,只是对 Spring 中的 ApplicationContext 做了非空验证。执行该测试用例,我们从输出的控制台信息中可以看到 Spring Boot 应用程序被正常启动,同时测试用例本身也会给出执行成功的提示。

上述测试用例虽然简单,但已经包含了测试 Spring Boot 应用程序的基本代码框架。这里面最重要的就是 ApplicationContextTests 类上的 @SpringBootTest 和 @RunWith 注解,我接下来就要对这两个注解进行详细介绍。对于 Spring Boot 应用程序而言,这两个注解构成了一套完整的测试方案。

解析基础测试注解

在导入 spring-boot-starter-test 依赖之后,我们就可以使用它所提供的各项功能来应对复杂的测试场景。spring-boot-starter-test 的强大之处在于它为我们提供了一批简单而有用的注解,这里我对常见的基础类测试注解做一下简要介绍。

因为 Spring Boot 程序的入口是 Bootstrap 类,Spring Boot 专门提供了一个 @SpringBootTest 注解来测试 Bootstrap 类。所有配置都会通过 Bootstrap 类去加载,而该注解可以引用 Bootstrap 类的配置。

在上面的例子中,我们直接通过 @SpringBootTest 注解所提供的默认功能对作为 Bootstrap 类的CustomerApplication类进行测试。而更常见的做法是在 @SpringBootTest 注解中指定该 Bootstrap 类,并设置测试的 Web 环境,如下所示。

java
@SpringBootTest(classes = CustomerApplication.class, 
	webEnvironment = SpringBootTest.WebEnvironment.MOCK)

@SpringBootTest 注解中的 webEnvironment 可以有四个选项,分别是 MOCK、RANDOM_PORT、DEFINED_PORT 和 NONE。

  • MOCK:加载 WebApplicationContext 并提供一个 Mock 的容器环境,内置的容器并没有真正启动。

  • RANDOM_PORT:加载 EmbeddedWebApplicationContext 并提供一个真实的容器环境,也就是说会启动内置容器,并使用随机端口。

  • DEFINED_PORT :这个配置也是通过加载 EmbeddedWebApplicationContext 提供一个真实的容器环境,但使用的是默认的端口,如果没有配置端口就使用 8080。

  • NONE:加载 ApplicationContext 但并不提供任何真实的容器环境。

在 Spring Boot 中,@SpringBootTest 注解主要用于测试基于自动配置的 ApplicationContext,它允许你设置测试上下文中的容器环境。在多数场景下,一个真实的容器环境对于测试而言过于重量级,通过 MOCK 环境可以缓解这种环境约束所带来的成本和挑战。我们在后续的课时中会结合 WebEnvironment.MOCK 选项来对服务层中的具体功能进行集成测试。

在上面的示例中,你还能看到一个 @RunWith 注解,该注解由 JUnit 框架提供,用于设置测试运行器,例如我们可以通过 @RunWith(SpringJUnit4ClassRunner.class) 让测试运行于 Spring 测试环境,这里我们指定的是 SpringRunner.class。SpringRunner 实际上就是对 SpringJUnit4ClassRunner 的简化,它允许 JUnit 和 Spring 测试上下文整合运行,而 Spring 测试上下文则提供了用于测试 Spring 应用程序的各项通用的支持功能。同样,我们在后续的测试用例中将大量使用 SpringRunner。

小结与预告

测试是一套独立的技术体系,需要开发人员充分重视且付诸实践。而与普通的应用程序测试过程相比,响应式应用程序测试的基础是对响应式流的测试。本讲首先基于 Reactor 框架提供的测试工具类介绍了针对响应式流的测试方法,并结合 Spring Boot 应用程序,阐述了全栈式的响应式测试方案以及对应的技术组件。

最后给你留一道思考题:在使用 Spring Boot 执行测试用例时,针对 Web 应用程序中各层组件的测试可以分别采用什么技术?

在介绍响应式流的测试技术以及全栈式的测试解决方案之后,下一讲我们将讨论如何分别对 Repository 层、Service 层以及 Controller 层的组件开展测试。到时候见。

点击链接,获取课程相关代码 ↓↓↓
https://github.com/lagoueduCol/ReactiveProgramming-jianxiang.git