1. Overview
1.概述
In this tutorial, we’ll discuss the enhanced Testcontainers support introduced in Spring Boot 3.1.
在本教程中,我们将讨论 Spring Boot 3.1 中引入的增强型 Testcontainers 支持。
This update provides a more streamlined approach to configuring the containers, and it allows us to start them for local development purposes. As a result, developing and running tests using Testcontainers becomes a seamless and efficient process.
此更新提供了一种更简化的容器配置方法,并允许我们为本地开发目的启动容器。因此,使用 Testcontainers 开发和运行测试将成为一个无缝、高效的过程。
2. Testcontainers Prior to SpringBoot 3.1
2.SpringBoot 3.1 之前的测试容器
We can use Testcontainers to create a production-like environment during the testing phase. By doing so, we’ll eliminate the need for mocks and write high-quality automated tests that aren’t coupled to the implementation details.
在测试阶段,我们可以使用 Testcontainers 创建一个类似生产的环境。这样,我们就不需要模拟,就能编写出与实现细节无关的高质量自动测试。
For the code examples in this article, we’ll use a simple web application with a MongoDB database as a persistence layer and a small REST interface:
在本文的代码示例中,我们将使用一个简单的网络应用程序,并将 MongoDB 数据库作为持久层和一个小型 REST 接口:
@RestController
@RequestMapping("characters")
public class MiddleEarthCharactersController {
private final MiddleEarthCharactersRepository repository;
// constructor not shown
@GetMapping
public List<MiddleEarthCharacter> findByRace(@RequestParam String race) {
return repository.findAllByRace(race);
}
@PostMapping
public MiddleEarthCharacter save(@RequestBody MiddleEarthCharacter character) {
return repository.save(character);
}
}
During the integration tests, we’ll spin up a Docker container containing the database server. Since the database port exposed by the container will be dynamically allocated, we cannot define the database URL in the properties file. As a result, for a Spring Boot application with a version prior to 3.1, we’d need to use @DynamicPropertySource annotation in order to add these properties to a DynamicPropertyRegistry:
在集成测试期间,我们将启动一个包含数据库服务器的 Docker 容器。由于容器暴露的数据库端口将动态分配,因此我们无法在属性文件中定义数据库 URL。因此,对于版本早于 3.1 的 Spring Boot 应用程序,我们需要使用 @DynamicPropertySource 注解,以便将这些属性添加到 DynamicPropertyRegistry 中:
@Testcontainers
@SpringBootTest(webEnvironment = DEFINED_PORT)
class DynamicPropertiesIntegrationTest {
@Container
static MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.0.10"));
@DynamicPropertySource
static void setProperties(DynamicPropertyRegistry registry) {
registry.add("spring.data.mongodb.uri", mongoDBContainer::getReplicaSetUrl);
}
// ...
}
For the integration test, we’ll use the @SpringBootTest annotation to start the application on the port defined in the configuration files. Additionally, we’ll use Testcontainers for setting up the environment.
对于集成测试,我们将使用 @SpringBootTest 注解在配置文件中定义的端口上启动应用程序。此外,我们还将使用 Testcontainers 来设置环境。
Finally, let’s use REST-assured for executing the HTTP requests and asserting the validity of the responses:
最后,让我们使用 REST-assured 来执行 HTTP 请求并断言响应的有效性:
@Test
void whenRequestingHobbits_thenReturnFrodoAndSam() {
repository.saveAll(List.of(
new MiddleEarthCharacter("Frodo", "hobbit"),
new MiddleEarthCharacter("Samwise", "hobbit"),
new MiddleEarthCharacter("Aragon", "human"),
new MiddleEarthCharacter("Gandalf", "wizzard")
));
when().get("/characters?race=hobbit")
.then().statusCode(200)
.and().body("name", hasItems("Frodo", "Samwise"));
}
3. Using @ServiceConnection for Dynamic Properties
3.使用 @ServiceConnection 获取动态属性
Starting with SpringBoot 3.1, we can utilize the @ServiceConnection annotation to eliminate the boilerplate code of defining the dynamic properties.
从 SpringBoot 3.1 开始,我们可以利用 @ServiceConnection 注解来消除定义动态属性的模板代码。
Firstly, we’ll need to include the spring-boot-testcontainers dependency in our pom.xml:
首先,我们需要在 pom.xml 中包含 spring-boot-testcontainers 依赖项:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
After that, we can remove the static method that registers all the dynamic properties. Instead, we’ll simply annotate the container with @ServiceConnection:
之后,我们可以删除注册所有动态属性的静态方法。相反,我们只需用 @ServiceConnection 对容器进行注解即可:
@Testcontainers
@SpringBootTest(webEnvironment = DEFINED_PORT)
class ServiceConnectionIntegrationTest {
@Container
@ServiceConnection
static MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.0.10"));
// ...
}
The @ServiceConncetion allows SpringBoot’s autoconfiguration to dynamically register all the needed properties. Behind the scenes, @ServiceConncetion determines which properties are needed based on the container class, or on the Docker image name.
@ServiceConncetion 允许 SpringBoot 的自动配置动态注册所有需要的属性。在幕后,@ServiceConncetion会根据容器类或 Docker 映像名称来确定需要哪些属性。
A list of all the containers and images that support this annotation can be found in Spring Boot’s official documentation.
Spring Boot 的 官方文档中列出了支持此注解的所有容器和映像。
4. Testcontainers Support for Local Development
4.Testcontainers 支持地方发展
Another exciting feature is the seamless integration of Testcontainers into local development with minimal configuration. This functionality enables us to replicate the production environment not only during testing but also for local development.
另一个令人兴奋的功能是将 Testcontainers 无缝集成到本地开发中,只需最少的配置。这一功能使我们不仅能在测试期间复制生产环境,还能用于本地开发。
In order to enable it, we first need to create a @TestConfiguration and declare all the Testcontainers as Spring Beans. Let’s also add the @ServiceConnection annotation that will seamlessly bind the application to the database:
为了启用它,我们首先需要创建一个 @TestConfiguration 并将所有测试容器声明为 Spring Bean。我们还要添加 @ServiceConnection 注解,以便将应用程序无缝绑定到数据库:
@TestConfiguration(proxyBeanMethods = false)
class LocalDevTestcontainersConfig {
@Bean
@ServiceConnection
public MongoDBContainer mongoDBContainer() {
return new MongoDBContainer(DockerImageName.parse("mongo:4.0.10"));
}
}
Because all the Testcontainers dependencies are being imported with a test scope, we’ll need to start the application from the test package. Consequently, let’s create in this package a main() method that calls the actual main() method from the java package:
由于所有 Testcontainers 依赖项都是以 test 范围导入的,因此我们需要从 test 包中启动应用程序。因此,让我们在该包中创建一个 main() 方法,该方法将调用 java 包中的实际 main() 方法:
public class LocalDevApplication {
public static void main(String[] args) {
SpringApplication.from(Application::main)
.with(LocalDevTestcontainersConfig.class)
.run(args);
}
}
This is it. Now we can start the application locally from this main() method and it will use the MongoDB database.
就是这样。现在,我们可以通过此 main() 方法在本地启动应用程序,它将使用 MongoDB 数据库。
Let’s send a POST request from Postman and then directly connect to the database and check if the data was correctly persisted:
让我们从 Postman 发送一个 POST 请求,然后直接连接到数据库,检查数据是否被正确持久化:
In order to connect to the database, we’ll need to find the port exposed by the container. We can fetch it from the application logs or simply by running the docker ps command:
为了连接数据库,我们需要找到容器暴露的端口。我们可以从应用程序日志中获取,或者直接运行 docker ps 命令:
Finally, we can use a MongoDB client to connect to the database using the URL mongodb://localhost:63437/test, and query the characters collection:
最后,我们可以使用 MongoDB 客户端通过 URL mongodb://localhost:63437/test 连接到数据库,并查询 characters 集合:
That’s it, we’re able to connect and query to the database started by the Testcontainer for local development.
就这样,我们就可以连接并查询由 Testcontainer 启动的数据库,进行本地开发了。
5. Integration With DevTools and @RestartScope
5.与 DevTools 和 @RestartScope 集成
If we restart the application often during the local development, a potential downside would be that all the containers will be restarted each time. As a result, the start-up will potentially be slower and the test data will be lost.
如果我们在本地开发过程中经常重启应用程序,潜在的弊端就是每次都要重启所有容器。因此,启动速度可能会变慢,测试数据也会丢失。
However, we can keep containers alive when the application is being reloaded by leveraging the Testcontainers integration with spring-boot-devtools. This is an experimental Testcontainers feature that enables a smoother and more efficient development experience, as it saves valuable time and test data.
不过,通过利用 Testcontainers 与 spring-boot-devtools 的集成,我们可以在应用程序重新加载时保持容器存活。这是一项试验性的 Testcontainers 功能,可节省宝贵的时间和测试数据,从而带来更流畅、更高效的开发体验。
Let’s start by adding the spring-boot-devtools dependency:
首先,让我们添加 spring-boot-devtools 依赖关系:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
Now, we can go back to the test configuration for local development and annotate the Testcontainers beans with the @RestartScope annotation:
现在,我们可以返回本地开发的测试配置,并使用 @RestartScope 注解注释 Testcontainers Bean:
@Bean
@RestartScope
@ServiceConnection
public MongoDBContainer mongoDBContainer() {
return new MongoDBContainer(DockerImageName.parse("mongo:4.0.10"));
}
As a result, we can now start the application from the main() method from the test package and take advantage of the spring-boot-devtools live-reload functionality. For instance, we can save an entry from Postman, then recompile and reload the application:
因此,我们现在可以从 test 软件包中的 main() 方法启动应用程序,并利用 spring-boot-devtools 实时重载功能。例如,我们可以从 Postman 中保存一个条目,然后重新编译并重新加载应用程序:
Let’s introduce a minor change like switching the request mapping from “characters” to “api/characters” and re-compile:
让我们引入一个小改动,比如将请求映射从 “characters” 改为 “api/characters” 并重新编译:
We can already see from the application logs or from Docker itself that the database container wasn’t restarted. Nevertheless, let’s go one step further and check that the application reconnected to the same database after the restart. For example, we can do this by sending a GET request at the new path and expecting the previously inserted data to be there:
我们可以从应用程序日志或 Docker 本身看到,数据库容器并未重启。不过,让我们更进一步,检查应用程序是否在重启后重新连接到同一个数据库。例如,我们可以在新路径下发送 GET 请求,并期待之前插入的数据会出现在那里:
Similarly, we can use the withReuse(true) method of the Testcontainer’s API:
同样,我们可以使用 Testcontainer API 的 withReuse(true) 方法:
@Bean
@ServiceConnection
public MongoDBContainer mongoDBContainer() {
return new MongoDBContainer(DockerImageName.parse("mongo:4.0.10"))
.withReuse(true);
}
This is a more powerful alternative that enables the container to outlive the application. In other words, by enabling reuse, we can reload or even completely restart the application, while ensuring the containers remain actively preserved.
这是一种功能更强大的替代方案,可使容器的寿命超过应用程序的寿命。换句话说,通过启用重用,我们可以重新加载或甚至完全重启应用程序,同时确保容器保持活跃状态。
6. Conclusion
6.结论
In this article, we’ve discussed SpringBoot 3.1’s new Testcontainers features. We learned how to use the new @ServiceConnection annotation that provides a streamlined alternative to using @DynamicPropertySource and the boilerplate configuration.
在本文中,我们讨论了 SpringBoot 3.1 的新 Testcontainers 功能。我们了解了如何使用新的 @ServiceConnection 注解,该注解为使用 @DynamicPropertySource 和模板配置提供了简化的替代方案。
Following that, we delved into utilizing Testcontainers for local development by creating an additional main() method in the test package and declaring them as Spring beans. In addition to this, the integration with spring-boot-devtools and @RestartScope enabled us to create a fast, consistent, and reliable environment for local development.
随后,我们在 test 包中创建了一个额外的 main() 方法,并将其声明为 Spring Bean,从而深入研究了如何利用 Testcontainers 进行本地开发。除此之外,与 spring-boot-devtools 和 @RestartScope 的集成使我们能够为本地开发创建快速、一致和可靠的环境。
As always, the complete code used in this article is available over on GitHub.
与往常一样,本文中使用的完整代码可在 GitHub 上获取。