如果您使用 Testcontainers JUnit 5 扩展将容器与 Spring Boot 测试集成,您最终会遇到两个系统尝试在整个生命周期内管理资源的场景,这并不理想。
@TestConfiguration已经解决这个问题。
以这个应用程序为例:
import java.util.List; import java.util.UUID; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @SpringBootApplication @EnableNeo4jRepositories(considerNestedRepositories = true) public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } @Node public record Movie(@Id @GeneratedValue(GeneratedValue.UUIDGenerator.class) String id, String title) { Movie(String title) { this(UUID.randomUUID().toString(), title); } } interface MovieRepository extends Neo4jRepository<Movie, String> { } @RestController static class MovieController { private final MovieRepository movieRepository; public MovieController(MovieRepository movieRepository) { this.movieRepository = movieRepository; } @GetMapping("/movies") public List<Movie> getMovies() { return movieRepository.findAll(); } } }
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-testcontainers</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>neo4j</artifactId> <scope>test</scope> </dependency>
|
我们使用 @TestConfiguration 提供额外的测试配置。
与 @Configuration 相比,@TestConfiguration 有两方面的优势:
与普通的 @Configuration 类不同,它不会阻止 @SpringBootConfiguration 的自动检测。
还有:除非它是测试类的内部静态类,否则必须明确导入。
下面的代码有一个@ServiceConnection的@Bean方法。
该方法返回一个 Neo4jContainer Testcontainer。该容器被标记为可重用。
由于默认情况下我们不会关闭该资源,所以我们让 Testcontainers 来清理它。
当标记为可重复使用时,它将保持活性,这意味着第二次测试运行会更快。
下面定义为上下文提供了足够的信息,因此 Spring 可以调用该容器,将 Neo4j 的所有连接都重新连接到该容器上,这样一切就都能正常运行了。
import java.util.Map; import org.springframework.boot.devtools.restart.RestartScope; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.context.annotation.Bean; import org.testcontainers.containers.Neo4jContainer; @TestConfiguration(proxyBeanMethods = false) public class ContainerConfig { @Bean @ServiceConnection @RestartScope public Neo4jContainer<?> neo4jContainer() { return new Neo4jContainer<>("neo4j:5") .withLabels(Map.of("com.testcontainers.desktop.service", "neo4j")) .withReuse(true); } }
|
具体操作如下。请注意如何导入配置,以及没有使用属性。
import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; @SpringBootTest @Import(ContainerConfig.class) class MyApplicationTests { @Test void repositoryIsConnectedAndUsable( @Autowired MyApplication.MovieRepository movieRepository ) { var movie = movieRepository.save(new MyApplication.Movie("Barbieheimer")); assertThat(movie.id()).isNotNull(); } }
|
平心而论,官方文档中还有很多其他方法,但我最喜欢这种方法。它非常明确,而且只使用一套注释(来自 Spring),因此简洁明了。
现在谈谈 @RestartScope。
该注解的存在是有原因的:
- 您的类路径上可能有 Springs devtools,它将在必要时重启上下文。
- 当上下文重启时,容器也会重启,从而使可重用标记失效。
- 注解会保留原始 Bean。
这有什么意义?
新的 Testcontainers 支持与 Quarkus 引入的 "开发人员服务 "概念配合得非常好。
最初,我们只想进行测试驱动开发,但后来发生了一些事情,事情变得越来越仓促,最终,探索性工作变得非常有趣:因此,将您的应用程序与数据库或服务的运行实例结合在一起,感觉就像 "附带电池 "一样,会让您的工作效率非常高。
org.springframework.boot.SpringApplication 有一个新的 with 方法,用于使用附加配置来增强自动配置的应用程序。我们可以像这样在测试范围中的附加主类中使用上述 ContainerConfig:
import org.springframework.boot.SpringApplication; public class MyApplicationWithDevServices { public static void main(String[] args) { SpringApplication.from(MyApplication::main) .with(ContainerConfig.class) .run(args); } }
|
启动该应用程序后,向 http://localhost:8080/movies 提出的请求会立即生效,并连接到容器中运行的 Neo4j 实例。
现在是最后的精彩时刻,我在容器上添加的不祥标签怎么样了?
我是 Testcontainers Cloud 的忠实用户,我的机器上运行着他们的服务。这会自动将任何 Testcontainers 容器请求重定向到云上,我的机器上就不需要 Docker 了。
从几天前开始,可以为在云中运行的容器定义固定端口映射,如此处所述 设置固定端口,轻松调试开发服务。
我的机器配置如下:
more /Users/msimons/.config/testcontainers/services/neo4j.toml # This example selects neo4j instances and forwards port 7687 to 7687 on the client. # Same for the Neo4j HTTP port # Instances are found by selecting containers with label "com.testcontainers.desktop.service=neo4j". # ports defines which ports to proxy. # local-port indicates which port to listen on the client machine. System ports (0 to 1023) are not supported. # container-port indicates which port to proxy. If unset, container-port will default to local-port. ports = [ {local-port = 7687, container-port = 7687}, {local-port = 7474, container-port = 7474} ]
|
这样,我就可以在众所周知的 Neo4j 端口下访问由 MyApplicationWithDevServices 启动的 Neo4j 实例,从而实现以下功能:
# Use Cypher-Shell to create some data cypher-shell -uneo4j -ppassword "CREATE (:Movie {id: randomUuid(), title: 'Dune 2'})" # 0 rows # ready to start consuming query after 15 ms, results consumed after another 0 ms # Added 1 nodes, Set 2 properties, Added 1 labels # Request the data from the application running with dev services http localhost:8080/movies # HTTP/1.1 200 # Connection: keep-alive # Content-Type: application/json # Date: Thu, 27 Jul 2023 13:32:58 GMT # Keep-Alive: timeout=60 # Transfer-Encoding: chunked # # [ # { # "id": "824ec97e-0a97-4516-8189-f0bf5eb215fe", # "title": "Dune 2" # } # ]
|