Merge pull request #44187 from thecooldrop

* pr/44187:
  Polish 'Document methods of starting Testcontainer containers'
  Document methods of starting Testcontainer containers

Closes gh-44187
This commit is contained in:
Phillip Webb 2025-05-05 22:10:10 -07:00
commit 24f11ae48f
17 changed files with 356 additions and 14 deletions

View File

@ -75,6 +75,7 @@ url-spring-data-rest-docs=https://docs.spring.io/spring-data/rest/reference/{ant
url-spring-data-rest-site=https://spring.io/projects/spring-data-rest
url-spring-data-rest-javadoc=https://docs.spring.io/spring-data/rest/docs/{dotxversion-spring-data-rest}/api
url-spring-data-site=https://spring.io/projects/spring-data
url-testcontainers-docs=https://java.testcontainers.org
url-testcontainers-activemq-javadoc=https://javadoc.io/doc/org.testcontainers/activemq/{version-testcontainers-activemq}
url-testcontainers-cassandra-javadoc=https://javadoc.io/doc/org.testcontainers/cassandra/{version-testcontainers-cassandra}
url-testcontainers-couchbase-javadoc=https://javadoc.io/doc/org.testcontainers/couchbase/{version-testcontainers-couchbase}

View File

@ -5,12 +5,93 @@ The https://www.testcontainers.org/[Testcontainers] library provides a way to ma
It integrates with JUnit, allowing you to write a test class that can start up a container before any of the tests run.
Testcontainers is especially useful for writing integration tests that talk to a real backend service such as MySQL, MongoDB, Cassandra and others.
Testcontainers can be used in a Spring Boot test as follows:
In following sections we will describe some of the methods you can use to integrate Testcontainers with your tests.
include-code::vanilla/MyIntegrationTests[]
This will start up a docker container running Neo4j (if Docker is running locally) before any of the tests are run.
In most cases, you will need to configure the application to connect to the service running in the container.
[[testing.testcontainers.spring-beans]]
== Using Spring Beans
The containers provided by Testcontainers can be managed by Spring Boot as beans.
To declare a container as a bean, add a javadoc:org.springframework.context.annotation.Bean[format=annotation] method to your test configuration:
include-code::MyTestConfiguration[]
You can then inject and use the container by importing the configuration class in the test class:
include-code::MyIntegrationTests[]
TIP: This method of managing containers is often used in combination with xref:#testing.testcontainers.service-connections[service connection annotations].
[[testing.testcontainers.junit-extension]]
== Using the JUnit Extension
Testcontainers provides a JUnit extension which can be used to manage containers in your tests.
The extension is activated by applying the javadoc:org.testcontainers.junit.jupiter.Testcontainers[format=annotation] annotation from Testcontainers to your test class.
You can then use the javadoc:org.testcontainers.junit.jupiter.Container[format=annotation] annotation on static container fields.
The javadoc:org.testcontainers.junit.jupiter.Testcontainers[format=annotation] annotation can be used on vanilla JUnit tests, or in combination with javadoc:org.springframework.boot.test.context.SpringBootTest[format=annotation]:
include-code::MyIntegrationTests[]
The example above will start up a Neo4j container before any of the tests are run.
The lifecycle of the container instance is managed by Testcontainers, as described in {url-testcontainers-docs}/test_framework_integration/junit_5/#extension[their official documentation].
NOTE: In most cases, you will additionally need to configure the application to connect to the service running in the container.
[[testing.testcontainers.importing-configuration-interfaces]]
== Importing Container Configuration Interfaces
A common pattern with Testcontainers is to declare the container instances as static fields in an interface.
For example, the following interface declares two containers, one named `mongo` of type javadoc:org.testcontainers.containers.MongoDBContainer[] and another named `neo4j` of type javadoc:org.testcontainers.containers.Neo4jContainer.Neo4jContainer[]:
include-code::MyContainers[]
When you have containers declared in this way, you can reuse their configuration in multiple tests by having the test classes implement the interface.
It's also possible to use the same interface configuration in your Spring Boot tests.
To do so, add javadoc:org.springframework.boot.testcontainers.context.ImportTestcontainers[format=annotation] to your test configuration class:
include-code::MyTestConfiguration[]
[[testing.testcontainers.lifecycle]]
== Lifecycle of Managed Containers
If you have used the annotations and extensions provided by Testcontainers, then the lifecycle of container instances is managed entirely by Testcontainers.
Please refer to the {url-testcontainers-docs}[offical Testcontainers documentation] for the information.
When the containers are managed by Spring as beans, then their lifecycle is managed by Spring:
* Container beans are created and started before all other beans.
* Container beans are stopped after the destruction of all other beans.
This process ensures that any beans, which rely on functionality provided by the containers, can use those functionalities.
It also ensures that they are cleaned up whilst the container is still available.
TIP: When your application beans rely on functionality of containers, prefer configuring the containers as Spring beans to ensure the correct lifecycle behavior.
NOTE: Having containers managed by Testcontainers instead of as Spring beans provides no guarantee of the order in which beans and containers will shutdown.
It can happen that containers are shutdown before the beans relying on container functionality are cleaned up.
This can lead to exceptions being thrown by client beans, for example, due to loss of connection.
Container beans are created and started once per application context managed by Spring's TestContext Framework.
For details about how TestContext Framework manages the underlying application contexts and beans therein, please refer to the {url-spring-framework-docs}[Spring Framework documentation].
Container beans are stopped as part of the TestContext Framework's standard application context shutdown process.
When the application context gets shutdown, the containers are shutdown as well.
This usually happens after all tests using that specific cached application context have finished executing.
It may also happen earlier, depending on the caching behavior configured in TestContext Framework.
NOTE: A single test container instance can, and often is, retained across execution of tests from multiple test classes.

View File

@ -0,0 +1,31 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.testing.testcontainers.importingconfigurationinterfaces;
import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.junit.jupiter.Container;
interface MyContainers {
@Container
MongoDBContainer mongoContainer = new MongoDBContainer("mongo:5.0");
@Container
Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:5");
}

View File

@ -0,0 +1,26 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.testing.testcontainers.importingconfigurationinterfaces;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.context.ImportTestcontainers;
@TestConfiguration(proxyBeanMethods = false)
@ImportTestcontainers(MyContainers.class)
class MyTestConfiguration {
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.docs.testing.testcontainers.vanilla;
package org.springframework.boot.docs.testing.testcontainers.junitextension;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.Neo4jContainer;
@ -32,7 +32,7 @@ class MyIntegrationTests {
@Test
void myTest() {
// ...
/**/ System.out.println(neo4j);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -34,7 +34,7 @@ class MyIntegrationTests {
@Test
void myTest() {
// ...
/**/ System.out.println(neo4j);
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.testing.testcontainers.springbeans;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.MongoDBContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
@SpringBootTest
@Import(MyTestConfiguration.class)
class MyIntegrationTests {
@Autowired
private MongoDBContainer mongo;
@Test
void myTest() {
/**/ System.out.println(this.mongo);
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.testing.testcontainers.springbeans;
import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.utility.DockerImageName;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
@TestConfiguration(proxyBeanMethods = false)
class MyTestConfiguration {
@Bean
MongoDBContainer mongoDbContainer() {
return new MongoDBContainer(DockerImageName.parse("mongo:5.0"));
}
}

View File

@ -29,7 +29,7 @@ class MyIntegrationTests {
@Test
fun myTest() {
// ...
/**/ println()
}
companion object {

View File

@ -0,0 +1,35 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.testing.testcontainers.importingconfigurationinterfaces
import org.testcontainers.containers.MongoDBContainer
import org.testcontainers.containers.Neo4jContainer
import org.testcontainers.junit.jupiter.Container
interface MyContainers {
companion object {
@Container
val mongoContainer: MongoDBContainer = MongoDBContainer("mongo:5.0")
@Container
val neo4jContainer: Neo4jContainer<*> = Neo4jContainer("neo4j:5")
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.testing.testcontainers.importingconfigurationinterfaces
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.boot.testcontainers.context.ImportTestcontainers
@TestConfiguration(proxyBeanMethods = false)
@ImportTestcontainers(MyContainers::class)
class MyTestConfiguration {
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.docs.testing.testcontainers.vanilla
package org.springframework.boot.docs.testing.testcontainers.junitextension
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.Neo4jContainer;
@ -22,7 +22,6 @@ import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection
@Testcontainers
@SpringBootTest
@ -30,13 +29,15 @@ class MyIntegrationTests {
@Test
fun myTest() {
// ...
/**/ println()
}
companion object {
@Container
@JvmStatic
val neo4j = Neo4jContainer("neo4j:5");
}
}

View File

@ -30,7 +30,7 @@ class MyIntegrationTests {
@Test
fun myTest() {
// ...
/**/ println()
}
companion object {

View File

@ -23,9 +23,11 @@ import org.testcontainers.containers.GenericContainer
@TestConfiguration(proxyBeanMethods = false)
class MyRedisConfiguration {
@Bean
@ServiceConnection(name = "redis")
fun redisContainer(): GenericContainer<*> {
return GenericContainer("redis:7")
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.testing.testcontainers.springbeans
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
import org.testcontainers.containers.MongoDBContainer
@SpringBootTest
@Import(MyTestConfiguration::class)
class MyIntegrationTests {
@Autowired
private val mongo: MongoDBContainer? = null
@Test
fun myTest() {
/**/ println()
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.docs.testing.testcontainers.springbeans
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.testcontainers.containers.MongoDBContainer
import org.testcontainers.utility.DockerImageName
@TestConfiguration(proxyBeanMethods = false)
class MyTestConfiguration {
@Bean
fun mongoDbContainer(): MongoDBContainer {
return MongoDBContainer(DockerImageName.parse("mongo:5.0"))
}
}

View File

@ -82,6 +82,7 @@
<suppress files="spring-boot-configuration-processor[\\/]src[\\/]test[\\/]java[\\/]org[\\/]springframework[\\/]boot[\\/]configurationsample[\\/]" checks="SpringDeprecatedCheck"/>
<suppress files="ImportTestcontainersTests\.java" checks="InterfaceIsType" />
<suppress files="MyContainers\.java" checks="InterfaceIsType" />
<suppress files="MyInterface\.java" checks="InterfaceIsType" />
<suppress files="SpringBootBanner\.java" checks="SpringLeadingWhitespace" />
<suppress files="LoadTimeWeaverAwareConsumerContainers\.java" checks="InterfaceIsType" />
</suppressions>