Spring Boot 微服务集成测试: Testcontainers 实践指南
2025-01-13 04:00:08
Spring Boot 微服务集成测试与 Testcontainers
在微服务架构中,集成测试变得尤为关键,它可以验证服务之间的交互是否正确。Testcontainers 是一个强大的 Java 库,用于在 Docker 容器中运行各种服务。本文将讨论如何在 Spring Boot 微服务环境中使用 Testcontainers 进行集成测试,尤其是当多个微服务都需要数据库支持时。
问题:跨微服务的数据库依赖
典型场景下,一个微服务(Service A)可能需要依赖另一个微服务(Service B)的数据。在进行集成测试时,我们需要保证两个服务都运行起来,并且它们各自的数据源也已准备好。这引入一个挑战:如何在本地测试环境中为每个微服务配置和管理独立的数据库实例,并且协调它们之间的交互?
例如,在上面提供的代码案例中, Service A 在创建 Article 时会通过 FeignClient 调用 Service B 的接口获取 Supplier 信息,并且两个 Service 都使用 mysql。
方案一:独立的 Testcontainers 实例
最直接的方法是为每个需要数据库的微服务设置独立的 Testcontainers 实例。这样做确保每个服务都有自己隔离的数据库环境,并且允许并行测试执行。
步骤:
-
为每个服务定义 Docker 容器 : 在 Service A 和 Service B 的测试代码中,分别创建各自的
MySQLContainer
实例,指定各自的数据库和连接配置。- Service A 的集成测试配置:
@ServiceConnection @Container private static final MySQLContainer<?> serviceAMySQLContainer = new MySQLContainer<>("mysql:8.0.36") .withDatabaseName("service_a_db") .withUsername("testuser") .withPassword("testpass");
- Service B 的集成测试配置:
@ServiceConnection @Container private static final MySQLContainer<?> serviceBMySQLContainer = new MySQLContainer<>("mysql:8.0.36") .withDatabaseName("service_b_db") .withUsername("testuser") .withPassword("testpass");
- Service A 的集成测试配置:
-
配置 Spring Boot 应用的连接属性 : 将 Service A 中
application.properties
配置成serviceAMySQLContainer.getJdbcUrl()
, 并修改数据库用户和密码为之前配置的。在 Service B 中,使用serviceBMySQLContainer.getJdbcUrl()
, 修改连接配置。确保程序读取到的数据库地址、用户名和密码跟容器一致。 -
服务依赖的配置 : 为了Service A可以正确请求Service B,需要明确配置好Feign Client对应的
application.properties
# Feign Client config
supplier-service.ribbon.listOfServers=localhost:${serviceB.port}
将`${serviceB.port}`配置成 `serviceBMySQLContainer.getFirstMappedPort()`
- 启动容器 : 使用
beforeAll
生命周期方法启动两个服务容器, 在每个测试方法运行前,确保数据库容器启动。
@BeforeAll
static void beforeAll() {
serviceAMySQLContainer.start();
serviceBMySQLContainer.start();
}
@AfterAll
static void afterAll() {
serviceAMySQLContainer.stop();
serviceBMySQLContainer.stop();
}
-
执行集成测试 : 编写集成测试代码,模拟服务间的交互,验证业务逻辑正确性。
@Test @DisplayName("Create articles /articles") void WHEN_calling_create_articles_THEN_status_is_200() { final var supplierDTO = new SupplierDTO(); supplierDTO.setId(1); final List<ArticleDTO> articleDTOS = Instancio.ofList(ArticleDTO.class).size(3) .ignore(field(ArticleDTO::getId)) .ignore(field(ArticleDTO::getCategory)) .set(field(ArticleDTO::getSupplier),supplierDTO) .create(); final CreateArticlesRequest createArticlesRequest = CreateArticlesRequest.builder() .articles(articleDTOS) .user(Instancio.create(String.class)) .build(); final ResponseEntity<CreateArticlesResponse> response = restTemplate.exchange(restTemplate.getRootUri() + URIConstants.ARTICLES_URI, HttpMethod.POST, new HttpEntity<>(createArticlesRequest), new ParameterizedTypeReference<>() {}); final List<ArticleDTO> articles = Objects.requireNonNull(response.getBody()).getArticles(); articles.forEach(article -> article.setId(null)); assertEquals(articles.size(), articleDTOS.size() , "articles.size() should be " + articleDTOS.size()); assertTrue(articles.containsAll(articleDTOS), "articles should contain the same elements as articulosDTOS"); assertEquals(HttpStatus.OK, response.getStatusCode(), "Status Code should be " + HttpStatus.OK); }
优点:
- 隔离性 :每个服务有自己的独立数据库,避免测试间的数据干扰。
- 并行执行 : 可以并发执行集成测试,加快测试速度。
缺点:
- 配置较多 : 对于每个服务需要单独进行Testcontainers的配置,当微服务增加时,维护成本增加。
方案二:共享网络模式(高级)
在一些高级测试场景中,可以考虑使用 Testcontainers 的共享网络功能。 多个容器可以在同一个 Docker 网络中运行,允许服务之间通过容器名直接相互调用。
此方法较复杂,只简单概念。
步骤:
- 创建 Docker 网络。
- 使用相同的 Docker 网络启动所有容器。
- 通过容器名进行服务之间的调用, 例如 Service A 中配置 Service B 的地址为
http://<serviceb-container-name>:<port>
。 - 在需要时调整 DNS。
优点:
- 简化服务发现: 服务直接通过容器名调用,方便快捷。
缺点:
- 复杂度高: 配置较复杂,不适用于简单的场景。
额外的安全建议
- 使用
withTmpFs
为容器提供临时的内存文件系统,提升 IO 性能。 - 定期检查更新 Docker 镜像,避免使用存在漏洞的镜像。
- 不要将生产环境的配置信息泄露到测试环境中,例如使用测试专用的用户密码等。
Testcontainers 在 Spring Boot 微服务集成测试中非常实用。 使用上述的配置,你可以在隔离的环境中测试你的服务交互。记住,在进行微服务测试时,充分考虑环境的独立性和可靠性,从而提供更可信的测试结果。
请注意,由于每个服务之间都存在差异,可能需要对这些解决方案做对应的适配调整。