spring响应式编程22 测试集成:响应式 Web 应用程序如何进行测试?

上一讲,我们探讨了针对响应式系统的测试解决方案,也介绍了测试 Reactor 响应式流的系统方法。那么这一讲的内容仍然聚焦于此,我们来看看如何测试 Web 三层架构中的各层组件,即 Repository 层、Service 层和 Controller 层。

与测试单纯的 Reactor 编程组件不同,Web 应用程序不同层的组件之间存在自上而下的依赖关系。因此,我们将从 Repository 层开始自下而上来开展测试工作,并将对这些组件的测试使用不同的方案和技术。

测试响应式 Repository 层组件

在“15 | MongoDB 集成:如何在响应式应用中访问 NoSQL 数据库”中,我完整地介绍了如何构建响应式数据访问层组件,并向你演示了 MongoDB 这款主流的响应式 NoSQL 数据库的使用方法。而这一讲我们先来聊聊如何对响应式数据访问层组件开展测试工作,同样将基于 MongoDB 来设计并执行测试用例。

与传统的关系型数据库一样,MongoDB 的测试也有两种主流的方法,一种是基于内置的嵌入式数据库,一种是基于真实的数据库。我们分别来看一下。

测试内嵌式 MongoDB

测试内嵌式 MongoDB 需要引入上一讲提到的 flapdoodle,这是一个内嵌式 MongoDB 数据库,与关系型数据库中所使用的 H2 内嵌式数据库类似,flapdoodle 允许我们在不使用真实 MongoDB 数据库的情况下编写测试用例并执行测试。

首先,在 Maven 中引入 flapdoodle 依赖的方法如下所示。

<dependency>
        <groupId>de.flapdoodle.embed</groupId>
        <artifactId>de.flapdoodle.embed.mongo</artifactId>
        <scope>test</scope>
</dependency>

想要测试 MongoDB,就需要引入一个新的测试注解,即 @DataMongoTest。因为 @DataMongoTest 注解会使用测试配置自动创建与 MongoDB 的连接以及 ReactiveMongoTemplate 工具类,它默认使用的就是基于 flapdoodle 的内嵌式 MongoDB 实例。

接下来,我们对 ReactiveSpringCSS 案例中所实现的 ReactiveAccountRepository 进行测试,使用 Account 作为领域对象。ReactiveAccountRepository 封装了对 MongoDB 的各种操作,代码如下所示。

@Repository
public interface ReactiveAccountRepository extends 
    ReactiveMongoRepository<Account, String>, ReactiveQueryByExampleExecutor<Account> {
 
    Mono<Account> findAccountByAccountName(String accountName);
}

现在我们编写测试类 EmbeddedAccountRepositoryTest,测试用例代码如下所示,@DataMongoTest 注解为我们自动嵌入了 flapdoodle 数据库。

@RunWith(SpringRunner.class)
@DataMongoTest
public class EmbeddedAccountRepositoryTest {
 
    @Autowired
    ReactiveAccountRepository repository;
 
    @Autowired
    ReactiveMongoOperations operations;
    
    @Before
    public void setUp() {
        operations.dropCollection(Account.class);
 
        operations.insert(new Account("Account1", "AccountCode1", "AccountName1"));
        operations.insert(new Account("Account2", "AccountCode2", "AccountName2"));
        
        operations.findAll(Account.class).subscribe(
               account -> {
                   System.out.println(account.getId()
               );}
        );
    }
    
    @Test
    public void testGetAccountByAccountName() {
        Mono<Account> account = repository.findAccountByAccountName("AccountName1");
 
        StepVerifier.create(account)
           .expectNextMatches(results -> {
               assertThat(results.getAccountCode()).isEqualTo("AccountCode1");
               assertThat(results.getAccountName()).isEqualTo("AccountName1");
               return true;
        });
    }
}

可以看到上述代码实际上是由两个部分组成,首先使用 ReactiveMongoOperations 进行数据的初始化操作,你同样已经在第 15 讲中看到过类似的操作。然后,我们调用 ReactiveAccountRepository 中的 findAccountByAccountName() 方法获取数据,并通过 StepVerifier 工具类执行测试,这里使用了 expectNextMatches() 方法来执行断言。

以上测试用例的编写和执行都比较简单,为了验证 @DataMongoTest 注解是否生效以及 flapdoodle 数据库中具体生成的数据,我们有必要对测试用例执行过程中产生的日志进行分析,相关日志如下所示(为了方便查看,我只选取了日志中的一部分内容)。

	…
	[localhost:63506] org.mongodb.driver.cluster               : Monitor thread successfully connected to server with description ServerDescription{address=localhost:63506, type=STANDALONE, state=CONNECTED, ok=true, version=ServerVersion{versionList=[3, 2, 2]}, minWireVersion=0, maxWireVersion=4, maxDocumentSize=16777216, roundTripTimeNanos=1720260}
	 [localhost:63506] org.mongodb.driver.cluster               : Discovered cluster type of STANDALONE
	[main] c.t.p. EmbeddedAccountRepositoryTest: Started EmbeddedAccountRepositoryTestin 16.732 seconds (JVM running for 19.935)
	 [Thread-6] o.s.b.a.mongo.embedded.EmbeddedMongo     : 2018-06-28T11:58:18.870+0800 I NETWORK  [initandlisten] connection accepted from 127.0.0.1:63520 #3 (3 connections now open)
	 [main] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:3, serverValue:3}] to localhost:63506
	 [Thread-6] o.s.b.a.mongo.embedded.EmbeddedMongo     : 2018-06-28T11:58:18.898+0800 I COMMAND  [conn3] CMD: drop test.account
	Account(id= Account1, accountCode= AccountCode1, accountName= AccountName1)
	Account(id= Account2, accountCode= AccountCode2, accountName= AccountName2)
	…

从上述日志中,我们可以清晰看到 flapdoodle 正在运作,并且成功完成了数据初始化操作。

下面再来看看另一种,测试基于真实的 MongoDB 数据库。

测试真实 MongoDB

测试真实 MongoDB 时我们不需要引入 flapdoodle 依赖,但同样需要使用 @ DataMongoTest注解。

接下来我们编写 LiveAccountRepositoryTest 类来对 ReactiveAccountRepository 进行测试,LiveAccountRepositoryTest 使用了真实的 MongoDB 数据库环境,代码如下所示。

@RunWith(SpringRunner.class)
@DataMongoTest(excludeAutoConfiguration = EmbeddedMongoAutoConfiguration.class)
public class LiveAccountRepositoryTest {
 
    @Autowired
    ReactiveAccountRepository repository;
 
    @Autowired
    ReactiveMongoOperations operations;
 
    @Before
    public void setUp() {
        operations.dropCollection(Account.class);
 
        operations.insert(new Account("Account1", "AccountCode1", "AccountName1"));
        operations.insert(new Account("Account2", "AccountCode2", "AccountName2"));
        
        operations.findAll(Account.class).subscribe(
               account -> {
                   System.out.println(account.getId()
               );}
        );
    }
    
    @Test
    public void testGetAccountByAccountName() {
        Mono<Account> account = repository.findAccountByAccountName("AccountName1");
 
        StepVerifier.create(account)
           .expectNextMatches(results -> {
               assertThat(results.getAccountCode()).isEqualTo("AccountCode1");
               assertThat(results.getAccountName()).isEqualTo("AccountName1");
               return true;
        });
    }
}

相较于 EmbeddedAccountRepositoryTest 类,LiveAccountRepositoryTest 类只有一个地方不同,即如下语句。

@DataMongoTest(excludeAutoConfiguration = 
	EmbeddedMongoAutoConfiguration.class)

事实上,@DataMongoTest 注解能使 Spring Boot 中默认使用真实 MongoDB 数据库的配置内容失效,而自动采用内嵌式的 flapdoodle 数据库。显然,为了测试真实环境的 MongoDB,我们需要把内嵌式的 flapdoodle 数据库转换到真实的 MongoDB 数据库。上述代码展示了这一场景下的具体做法,即使用 excludeAutoConfiguration 显式排除 EmbeddedMongoAutoConfiguration 配置。

测试响应式 Service 层组件

接下来,我们继续讨论三层架构中的中间层 Service 组件,并基于前面介绍的 ReactiveAccountRepository 类构建 Service 层组件并设计相应的测试用例。

我们首先需要完成 Service 层组件 AccountService 类的实现,代码如下所示。

@Service
public class AccountService {
    
    @Autowired
    private ReactiveAccountRepository accountRepository;
        
    public Mono<Account> getAccountById(String accountId) {
        
        return accountRepository.findById(accountId).log("getAccountById");
    }
    
    public Mono<Account> getAccountByAccountName(String accountName) {
        
        return accountRepository.findAccountByAccountName(accountName).log("getAccountByAccountName");
	}
}

对 AccountService 测试的难点在于如何隔离 ReactiveAccountRepository,即我们希望在不进行真实数据访问操作的前提下仍然能够验证 AccountService 中方法的正确性。尽管 AccountService 中的 getAccountByAccountName() 方法逻辑非常简单,只是对 ReactiveAccountRepository 中方法的封装,但从集成测试的角度讲,确保组件之间的隔离性是一条基本测试原则

以下代码演示了如何使用 Mock 机制完成对 ReactiveAccountRepository 的隔离。我们首先通过 @MockBean 注解注入 ReactiveAccountRepository,然后基于第三方 Mock 框架 mockito 提供的 given/willReturn 机制完成对 ReactiveAccountRepository 中 getAccountByAccountName() 方法的 Mock。

@RunWith(SpringRunner.class)
@SpringBootTest
public class AccountServiceTest {
 
    @Autowired
    AccountService service;
    
    @MockBean
    ReactiveAccountRepository repository;
 
    @Test
    public void testGetAccountByAccountName() {
        Account mockAccount = new Account("Account1", "AccountCode1", "AccountName1");
        
     given(repository.findAccountByAccountName("AccountName1")).willReturn(Mono.just(mockAccount));
        
        Mono<Account> account = service.getAccountByAccountName("AccountName1");
        
        StepVerifier.create(account).expectNextMatches(results -> {
            assertThat(results.getAccountCode()).isEqualTo("AccountCode1");
            assertThat(results.getAccountName()).isEqualTo("AccountName1");
           return true;
        }).verifyComplete();
        
    }   
}

在集成测试中,Mock 是一种常用策略。通过上述代码,你可以看到 Mock 的实现一般都会采用类似 mockito 的第三方框架,而具体 Mock 方法的行为则通过模拟的方式来实现。与使用 Stub 机制不同,对于某一个或一些被测试对象所依赖的测试方法而言,编写 Mock 相对简单,只需要模拟被使用的方法即可。


测试响应式 Controller 层组件

最后,我们再来讨论如何对位于最上层的 Controller 组件进行测试。我们同样基于它的下层组件 AccountService 来构建 AccountController,代码如下所示。

@RestController
@RequestMapping(value = "accounts")
public class AccountController {
 
    @Autowired
    private AccountService accountService;
 
    @GetMapping(value = "/{accountId}")
    public Mono<Account> getAccountById(@PathVariable("accountId") String accountId) {
 
        Mono<Account> account = accountService.getAccountById(accountId);
        return account;
    }
 
    @GetMapping(value = "accountname/{accountName}")
    public Mono<Account> getAccountByAccountName(@PathVariable("accountName") String accountName) {
 
        Mono<Account> account = accountService.getAccountByAccountName(accountName);
        return account;
    }
}

在测试 AccountController 类之前,我再给你介绍一个新的注解 @WebFluxTest,该注解是初始化测试 Controller 组件所必需的 WebFlux 基础设施。@WebFluxTest 注解的使用方法如下所示,这里我们构建了 AccountControllerTest 类来测试 AccountController。

@RunWith(SpringRunner.class)
@WebFluxTest(controllers = AccountController.class)
public class AccountControllerTest {
<span class="hljs-meta">@Autowired</span>
WebTestClient webClient;
…

}

默认情况下,@WebFluxTest 注解会确保所有包含 @RestController 注解的 JavaBean 生成一个 Mock 的 Web 环境,但我们也可以指定想要使用的具体 Controller 类,例如上述代码中就显式指定了 AccountController 作为我们测试的具体目标类。

同时,@WebFluxTest 注解会自动注入 WebTestClient 工具类。WebTestClient 工具类专门用来测试 WebFlux 组件,在使用上无须启动完整的 HTTP 容器。WebTestClient 工具类提供的常见方法有下面几种。

  • HTTP 请求方法:支持 get()、post() 等常见的 HTTP 方法构造测试请求,并使用 uri() 方法指定路由。

  • exchange() 方法:用于发起 HTTP 请求,返回一个 EntityExchangeResult。

  • expectStatus() 方法:用于验证返回状态,一般可以使用 isOk() 方法来验证是否返回 200 状态码。

  • expectBody() 方法:用于验证返回对象体是否为指定对象,并利用 returnResult() 方法获取对象。

AccountControllerTest 类的完整代码如下所示,我把与测试相关的 import 语句也列在了这里,以便你了解各种工具类的由来。

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.EntityExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
 
import com.springcss.account.controller.AccountController;
import com.springcss.account.domain.Account;
import com.springcss.account.service.AccountService;
 
import reactor.core.publisher.Mono;
 
@RunWith(SpringRunner.class)
@WebFluxTest(controllers = AccountController.class)
public class AccountControllerTest {
 
    @Autowired
    WebTestClient webClient;
 
    @MockBean
    AccountService service;
 
    @Test
    public void testGetAccountById() {
        Account mockAccount = new Account("Account1", "AccountCode1", "AccountName1");       
 
     given(service.getAccountById("Account1")).willReturn(Mono.just(mockAccount));
 
        EntityExchangeResult<Account> result = webClient.get()
               .uri("http://localhost:8082/accounts/{accountId}", "Account1").exchange().expectStatus()
               .isOk().expectBody(Account.class).returnResult();
 
        verify(service).getAccountById("Account1");
        verifyNoMoreInteractions(service);
        
     assertThat(result.getResponseBody().getId()).isEqualTo("Account1");
     assertThat(result.getResponseBody().getAccountCode()).isEqualTo("AccountCode1");
    }
}

上述代码中,我们首先通过 mockito 提供的 given/willReturn 机制完成对 AccountService 中相关方法的 Mock,然后通过 WebTestClient 工具类完成 HTTP 请求的发送和响应的解析。同时,你可以注意到,这里还使用了 mockito 中的 verify() 和 verifyNoMoreInteractions() 方法来验证 AccountService 在测试用例执行过程中的参与情况,这也是非常有用的一种实践技巧。

小结与预告

关于响应式 Web 服务的测试,我们需要考虑分层思想,即从数据流层出发分别对基于响应式的 Repository 层、Service 层以及 Controller 层进行测试。在整个测试过程中,测试注解发挥了核心作用。下表罗列了使用到的主要测试注解及其描述:

图片7 (1).png

最后还是给你留一道思考题:在使用 Spring Boot 测试 Web 应用程序时,常见的测试注解你知道有哪些吗?

至此,测试组件就介绍完了,我们的课程也将告一段落,恭喜你坚持到了最后。在下一讲结束语中,我将对 Spring 中的响应式编程技术进行总结,并对它的后续发展进行展望。

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


精选评论

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

源码头

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值