软件开发教父--Martin Fowler在其题为《微服务架构的测试策略》讲演中,详细诠释了测试不同级别的微服务的概念,其中就提到了如下图所示的“测试金字塔”模型。该模型从下到上分别为:单元、集成、组件、端到端和探索。
而不可否认的是,随着业界广泛采用云端微服务,我们在得益于处理多个可独立部署的组件的同时,需要提高微服务应用的测试级别,并按需增加测试策略的复杂性。下面,我将从使用者的角度出发,以一个Spring Cloud微服务为例,深入探究各种服务组件的相关测试。
服务
我们的Spring Boot微服务示例会具有如下特征:
- 将启用Spring Cloud Netflix。它会使用Spring Boot应用的Netflix OSS集成,来执行服务注册与发现、分布式外部配置(Spring Cloud Config)、以及客户端的负载平衡
- 将与关系型数据库(如PostgreSQL)相集成
- 将能调用另一个(内部)微服务
- 将调用第三方(外部)Web服务
- 将启用Spring Security,以充当OAuth2的资源服务器
- 将被“隐藏”在API网关服务器,例如Spring Cloud Gateway的后面
我们将通过Java 11、Apache Maven、Docker、以及一组协作库,“尽早地”在CI/CD管道中,进行单独的服务测试,而无需实际部署或占用其他服务、数据库、甚至是完整的测试环境资源。同时,您可以通过链接--https://github.com/kmandalas/spring-cloud-component-tests,在GitHub上获取该示例的所有代码。
该示例中的“订单跟踪”微服务是由一个Spring Controller、Service和Repository所组成。它公开了两个端点:
- GET/api/orders/{trackingNumber}/status:它通过给定的跟踪号,执行数据库查询,来获取相关订单;然后调用FulfillmentService的内部服务,来确定交付的状态;进而让最终外部服务根据状态,调用位置服务来实现定位。这是一个带有有效的JWT、且受保护的API调用。
- GET/api/orders:通过查询数据库,以列出所有订单。这是一个受到额外授权限制的、且受保护的API调用。它仅适用于具有“back-office”角色的用户。
组件测试
OrderControllerTest.java类将针对API提供的多种方法,来封装组件测试。例如,我们可以选用包括:Maven插件、JUnit功能、Spring Boot测试切片和分类单元测试、集成测试、组件测试、合同测试等方法。当然,并非所有的测试类别都需要在CI/CD管道中被执行(或重新执行)。鉴于该示例过于简单,我强烈建议您实施适当的分类。
在/src/test/resources/application.yml中,我们针对属性的测试配置如下:
YAML
server
port0
spring
application
name order-service-test
cloud
service-registry
auto-registration
enabledfalse
loadbalancer
ribbon
enabledfalse
config
enabledfalse
jpa
show-sqltrue
eureka
client
enabledfalse
service-url
registerWithEurekafalse
okta
oauth2
issuer https //kmandalas/oauth2/default
location-service
url http //localhost 9999/v1/track/
在上述代码段所示中,我们禁用spring.cloud.config、eureka.client和spring.cloud.service-registry.auto-registration的原因在于,方便孤立地测试微服务。因此,既不会有Spring Cloud Config服务器在启动时,为OrderService的配置属性提供服务;也不会有Eureka服务器提供注册,并能够使用它来按需调用FulfillmentService的动态服务发现。
数据库
当出于测试目的而必须与数据库(关系型或NoSQL)集成时,我们通常有如下三种选择:
- 使用嵌入式或内存中(in-memory)方案,例如:H2,https://www.h2database.com/
- 使用一个能在测试期间可供访问的真实数据库
- 使用与生产数据库接近甚至相同的临时数据库
不同的选项所涉及到的测试资源,将会不尽相同。
- 如果采用第一种方法,将H2进行集成和组件测试,那么由于生产环境的数据库很可能与H2不同,因此您将不得不维护各种独立的DDL和DML脚本。此外,您也可能会用到原生查询、或其他特定于某个数据库的功能。
- 如果您需要进行端到端或性能测试的话,那么就应该部署真实的数据库,并在测试环境中启动并运行它。对此,现代化的IaC(infrastructure as code,基础设施即代码)工具、以及详尽的测试数据管理,将可以为项目按需提供灵活性。
- 在本测试示例中,我们将使用第三种方法,利用testcontainers和Flyway,实现与Spring Boot的配合,而数据库才采用PostgreSQL。在testcontainers的帮助下,我们将在测试的初始化阶段,创建一个临时的dockerized数据库实例。而Flyway将会在这个临时模式(schema)上触发迁移脚本(DDL/DML),以便我们的代码将透明地、针对该临时模式运行。而在测试完成时,我们会处理掉这个dockerized数据库。
可见,我们实际上只需要OrderControllerTest类上的@Testcontainers注释,以及如下的静态声明:
Java
static PostgreSQLContainer database = new PostgreSQLContainer("postgres:12")
.withDatabaseName("tutorial")
.withUsername("kmandalas")
.withPassword("dzone2022");
static void setDatasourceProperties(DynamicPropertyRegistry propertyRegistry) {
propertyRegistry.add("spring.datasource.url", database::getJdbcUrl);
propertyRegistry.add("spring.datasource.password", database::getPassword);
propertyRegistry.add("spring.datasource.username", database::getUsername);
}
内部服务调用
我们将使用Spring Cloud OpenFeign来调用FulfillmentService,它是另一个“内部”的Spring Cloud微服务,可以被注册到Eureka上。在正常执行的情况下,后台的feign客户端能够通过名称定位目标服务实例,实现客户端的负载均衡(如果发现了多个实例的话)。
在我们的测试中,在没有Eureka(或者是Consul等其他发现机制)的情况下,我们需要通过如下两个方面,尽可能真实地模拟此类集成:
- 通过WireMock启动一个模拟服务器。该服务器能够根据URL的不同模式,来截获请求,并回复由我们提供的模拟响应。
- 使用@TestConfiguration来模拟各种FulfillmentService实例的发现,并将其指向WireMock服务器的URI。您可以通过链接--https://github.com/kmandalas/spring-cloud-component-tests/blob/50241126932fce3e9cfc6351291af5857f77806a/src/test/java/gr/kmandalas/dzone/OrderControllerTest.java#L55,查看到此类测试配置。
当然,您也可以使用Hoverfly作为嵌入式模拟服务器。在本示例里,我们通过如下依赖项设置,来引入WireMock:
XML
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
通过spring-cloud-starter-contract-stub-runner,WireMock在Spring Boot应用测试套件中的引导被简化了许多,同时这对于契约测试(contract tests)也是非常实用的。请通过查看Spring Cloud Contract WireMock的链接--https://docs.spring.io/spring-cloud-contract/docs/current/reference/html/project-features.html#features-wiremock,了解更多相关信息。
有了上面的基础,我们只需要使用@AutoConfigureWireMock去注释测试类,并在测试资源目录下的JSON文件中定义各种WireMock映射即可。
外部服务调用
在集成的过程中,为了能够调用某些外部的(第三方)服务,我们仍然需要依赖有效的WireMock映射(毕竟能够提供的响应多多益善),以便在application.yml中定义测试URL资源。下面是一个简单的示例:
YAML
location-service
url http //localhost 9999/v1/track/
我们在外部服务的URL端点路径处,提供了WireMock嵌入式服务器运行的主机和端口。端口号虽然不必经过硬编码,但是可以被定义为动态的,以便在CI/CD管道中并行运行多个组件测试,且不会发生端口冲突。
值得一提的是,WireMock不仅可以用于模拟来自RESTful服务的各种JSON响应,还可以模拟基于SOAP的Web服务的响应。
安全
正如前文提到的,Spring Cloud微服务基础设施通常能够合并出一个诸如Spring Cloud Gateway的API网关。据此,我们可以使用OAuth 2.0、JavaScript对象签名和加密(Object Signing and Encryption,JOSE)、以及JSON Web令牌标准的令牌中继模式,来处理用户的身份识别,授权应用程序查看他们的个人资料,以及访问网关后面的安全资源。通常,此类安全设置会由如下组件构成:
- 单点登录服务器,如Keycloak、Cloud Foundry 的用户帐户和身份验证服务器、以及诸如Okta之类商用的OAuth2身份验证提供程序。
- Spring Cloud Gateway之类的API网关服务器,将用户帐户的管理和授权委托给单点登录服务器。
- 资源服务器:在本Spring Boot微服务示例中为OrderService。
针对本测试示例,我们在单独测试Spring Boot微服务时,会采用Spring Security的SecurityMockMvcRequestPostProcessors。它将使我们能够在MockMvc调用期间,传递有效的JWT,定义权限(即用户角色),并在启用安全性的情况下,测试组件的行为。例如:
Java
mockMvc.perform(get("/api/orders/11212/status").with(jwt())).andExpect(status().isOk())
和
mockMvc.perform(get("/api/orders/").with(jwt().authorities(new
SimpleGrantedAuthority("backoffice"))))
.andExpect(status().isOk());
小结
如今,对于成功的产品交付而言,开发人员是否能够在CI/CD管道中,以自动化的方式执行各类测试是至关重要的。希望上述讨论的有关Spring Cloud微服务组件测试的相关指南和注意事项,能够给您的实际项目交付提供帮助。
译者介绍
陈峻 (Julian Chen),51CTO社区编辑,具有十多年的IT项目实施经验,善于对内外部资源与风险实施管控,专注传播网络与信息安全知识与经验;持续以博文、专题和译文等形式,分享前沿技术与新知;经常以线上、线下等方式,开展信息安全类培训与授课。
原文标题:Component Tests for Spring Cloud Microservices,作者:Kyriakos Mandalas和Dimitris Stavroulakis