diff --git a/spring-boot-project/spring-boot-docs/build.gradle b/spring-boot-project/spring-boot-docs/build.gradle index 9997ab1f73..3d045435c4 100644 --- a/spring-boot-project/spring-boot-docs/build.gradle +++ b/spring-boot-project/spring-boot-docs/build.gradle @@ -53,6 +53,7 @@ dependencies { autoConfiguration(project(path: ":spring-boot-project:spring-boot-autoconfigure", configuration: "autoConfigurationMetadata")) autoConfiguration(project(path: ":spring-boot-project:spring-boot-actuator-autoconfigure", configuration: "autoConfigurationMetadata")) autoConfiguration(project(path: ":spring-boot-project:spring-boot-devtools", configuration: "autoConfigurationMetadata")) + autoConfiguration(project(path: ":spring-boot-project:spring-boot-testcontainers", configuration: "autoConfigurationMetadata")) configurationProperties(project(path: ":spring-boot-project:spring-boot", configuration: "configurationPropertiesMetadata")) configurationProperties(project(path: ":spring-boot-project:spring-boot-actuator", configuration: "configurationPropertiesMetadata")) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc index 21b4733bc2..fe7900384f 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc @@ -1006,6 +1006,44 @@ The above configuration allows Neo4j-related beans in the application to communi +[[features.testing.testcontainers.at-development-time]] +==== Using Testcontainers at Development Time +As well as using Testcontainers for integration testing, it's also possible to use them at development time. +This approach allows developers to quickly start containers for the services that the application depends on, removing the need to manually provision things like database servers. +Using Testcontaners in this way provides functionality similar to Docker Compose, except that your container configuration is in Java rather than YAML. + +To use Testcontainers at development time you need to launch your application using your "`test`" classpath rather than "`main`". +This will allow you to access all declared test dependencies and give you a natural place to write your test configuration. + +To create a test launchable version of your application you should create an "`Application`" class in the `src/test` directory. +For example, if your main application is in `src/main/java/com/example/MyApplication.java`, you should create `src/test/java/com/example/TestMyApplication.java` + +The `TestMyApplication` class can use the `SpringApplication.from(...)` method to launch the real application: + +include::code:launch/TestMyApplication[] + +You'll also need to define the `Container` instances that you want to start along with your application. +To do this, you need to make sure that the `spring-boot-testcontainers` module has been added as a `test` dependency. +Once that has been done, you can create a `@TestConfiguration` class that declares `@Bean` methods for the containers you want to start. + +You can also annotate your `@Bean` methods with `@ServiceConnection` in order to create `ConnectionDetails` beans. +See <> section above for details of the supported technologies. + +A typical Testcontainers configuration would look like this: + +include::code:test/MyContainersConfiguration[] + +NOTE: The lifecycle of `Container` beans is automatically managed by Spring Boot. +Containers will be started and stopped automatically. + +Once you have defined your test configuration, you can use the `with(...)` method to attach it to your test launcher: + +include::code:test/TestMyApplication[] + +You can now launch `TestMyApplication` as you would any regular Java `main` method application to start your application and the containers that it needs to run. + + + [[features.testing.utilities]] === Test Utilities A few test utility classes that are generally useful when testing your application are packaged as part of `spring-boot`. diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/launch/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/launch/MyApplication.java new file mode 100644 index 0000000000..f93a2601ab --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/launch/MyApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2023 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.features.testing.testcontainers.atdevelopmenttime.launch; + +public class MyApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/launch/TestMyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/launch/TestMyApplication.java new file mode 100644 index 0000000000..782d5e6754 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/launch/TestMyApplication.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2023 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.features.testing.testcontainers.atdevelopmenttime.launch; + +import org.springframework.boot.SpringApplication; + +public class TestMyApplication { + + public static void main(String[] args) { + SpringApplication.from(MyApplication::main).run(args); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/test/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/test/MyApplication.java new file mode 100644 index 0000000000..e8339f1dae --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/test/MyApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2023 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.features.testing.testcontainers.atdevelopmenttime.test; + +public class MyApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/test/MyContainersConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/test/MyContainersConfiguration.java new file mode 100644 index 0000000000..df8852b447 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/test/MyContainersConfiguration.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2023 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.features.testing.testcontainers.atdevelopmenttime.test; + +import org.testcontainers.containers.Neo4jContainer; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; + +@TestConfiguration(proxyBeanMethods = false) +public class MyContainersConfiguration { + + @Bean + @ServiceConnection + public Neo4jContainer neo4jContainer() { + return new Neo4jContainer<>("neo4j:5"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/test/TestMyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/test/TestMyApplication.java new file mode 100644 index 0000000000..c65bf638ac --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/test/TestMyApplication.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2023 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.features.testing.testcontainers.atdevelopmenttime.test; + +import org.springframework.boot.SpringApplication; + +public class TestMyApplication { + + public static void main(String[] args) { + SpringApplication.from(MyApplication::main).run(args); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/launch/MyApplication.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/launch/MyApplication.kt new file mode 100644 index 0000000000..604343d60e --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/launch/MyApplication.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2022 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.features.testing.testcontainers.atdevelopmenttime.launch + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.docs.features.springapplication.MyApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class MyApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/launch/TestMyApplication.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/launch/TestMyApplication.kt new file mode 100644 index 0000000000..d4f2ea76cc --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/launch/TestMyApplication.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2023 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.features.testing.testcontainers.atdevelopmenttime.launch + +import org.springframework.boot.SpringApplication +import org.springframework.boot.docs.features.testing.testcontainers.atdevelopmenttime.launch.main as myApplicationMain + +object Main { + @JvmStatic + fun main(args: Array) { + SpringApplication.from(::myApplicationMain).run(*args) + } +} diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/test/MyApplication.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/test/MyApplication.kt new file mode 100644 index 0000000000..6207bfb2ba --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/test/MyApplication.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2023 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.features.testing.testcontainers.atdevelopmenttime.test + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.docs.features.springapplication.MyApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class MyApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/test/MyContainersConfiguration.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/test/MyContainersConfiguration.kt new file mode 100644 index 0000000000..26a3e420ec --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/test/MyContainersConfiguration.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2023 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.features.testing.testcontainers.atdevelopmenttime.test + +import org.testcontainers.containers.Neo4jContainer + +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.boot.testcontainers.service.connection.ServiceConnection +import org.springframework.context.annotation.Bean; + +@TestConfiguration(proxyBeanMethods = false) +class MyContainersConfiguration { + + @Bean + @ServiceConnection + fun neo4jContainer(): Neo4jContainer<*> { + return Neo4jContainer("neo4j:5") + } + +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/test/TestMyApplication.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/test/TestMyApplication.kt new file mode 100644 index 0000000000..0b7e4a17f1 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/testcontainers/atdevelopmenttime/test/TestMyApplication.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2023 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.features.testing.testcontainers.atdevelopmenttime.test + +import org.springframework.boot.SpringApplication +import org.springframework.boot.docs.features.testing.testcontainers.atdevelopmenttime.test.main as myApplicationMain + +object Main { + @JvmStatic + fun main(args: Array) { + SpringApplication.from(::myApplicationMain).with(MyContainersConfiguration::class.java).run(*args) + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/BeanOrigin.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/BeanOrigin.java new file mode 100644 index 0000000000..f4e3b94337 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/BeanOrigin.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2023 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.testcontainers.service.connection; + +import java.util.Objects; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.origin.Origin; + +/** + * {@link Origin} backed by a Spring Bean. + * + * @author Phillip Webb + */ +class BeanOrigin implements Origin { + + private final String beanName; + + private final BeanDefinition beanDefinition; + + BeanOrigin(String beanName, BeanDefinition beanDefinition) { + this.beanName = beanName; + this.beanDefinition = beanDefinition; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + BeanOrigin other = (BeanOrigin) obj; + return Objects.equals(this.beanName, other.beanName) && Objects + .equals(this.beanDefinition.getResourceDescription(), other.beanDefinition.getResourceDescription()); + } + + @Override + public int hashCode() { + return this.beanName.hashCode(); + } + + @Override + public String toString() { + String resourceDescription = this.beanDefinition.getResourceDescription(); + StringBuilder result = new StringBuilder(); + result.append("Bean '"); + result.append(this.beanName); + result.append("'"); + if (resourceDescription != null) { + result.append(" defined in "); + result.append(resourceDescription); + } + return result.toString(); + } + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionAutoConfiguration.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionAutoConfiguration.java new file mode 100644 index 0000000000..0089cb53cb --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionAutoConfiguration.java @@ -0,0 +1,97 @@ +/* + * Copyright 2012-2023 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.testcontainers.service.connection; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.testcontainers.containers.Container; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactories; +import org.springframework.boot.origin.Origin; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.Ordered; +import org.springframework.core.type.AnnotationMetadata; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} for {@link ServiceConnection @ServiceConnection} annotated + * {@link Container} beans. + * + * @author Phillip Webb + * @since 3.1.0 + */ +@AutoConfiguration +@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) +@Import(ServiceConnectionAutoConfiguration.Registrar.class) +public class ServiceConnectionAutoConfiguration { + + ServiceConnectionAutoConfiguration() { + } + + static class Registrar implements ImportBeanDefinitionRegistrar { + + private final BeanFactory beanFactory; + + Registrar(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, + BeanDefinitionRegistry registry) { + if (this.beanFactory instanceof ConfigurableListableBeanFactory listableBeanFactory) { + ConnectionDetailsFactories connectionDetailsFactories = new ConnectionDetailsFactories(); + List> sources = getSources(listableBeanFactory); + new ContainerConnectionSourcesRegistrar(listableBeanFactory, connectionDetailsFactories, sources) + .registerBeanDefinitions(registry); + } + } + + private List> getSources(ConfigurableListableBeanFactory beanFactory) { + List> sources = new ArrayList<>(); + for (String candidate : beanFactory.getBeanNamesForType(Container.class)) { + Set annotations = beanFactory.findAllAnnotationsOnBean(candidate, + ServiceConnection.class, false); + if (!annotations.isEmpty()) { + addSources(sources, beanFactory, candidate, annotations); + } + } + return sources; + } + + private void addSources(List> sources, + ConfigurableListableBeanFactory beanFactory, String beanName, Set annotations) { + BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName); + Origin origin = new BeanOrigin(beanName, beanDefinition); + Container container = beanFactory.getBean(beanName, Container.class); + for (ServiceConnection annotation : annotations) { + sources.add(new ContainerConnectionSource<>(beanName, origin, container, annotation)); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000..841426d00c --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.springframework.boot.testcontainers.service.connection.ServiceConnectionAutoConfiguration diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionAutoConfigurationTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionAutoConfigurationTests.java new file mode 100644 index 0000000000..6ada28cb67 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionAutoConfigurationTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2012-2023 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.testcontainers.service.connection; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisConnectionDetails; +import org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleApplicationContextInitializer; +import org.springframework.boot.testsupport.testcontainers.RedisContainer; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ServiceConnectionAutoConfiguration}. + * + * @author Phillip Webb + */ +class ServiceConnectionAutoConfigurationTests { + + private static final String REDIS_CONTAINER_CONNECTION_DETAILS = "org.springframework.boot.testcontainers.service.connection.redis." + + "RedisContainerConnectionDetailsFactory$RedisContainerConnectionDetails"; + + @Test + void whenNoExistingBeansRegistersServiceConnection() { + try (AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext()) { + applicationContext.register(WithNoExtraAutoConfiguration.class, ContainerConfiguration.class); + new TestcontainersLifecycleApplicationContextInitializer().initialize(applicationContext); + applicationContext.refresh(); + RedisConnectionDetails connectionDetails = applicationContext.getBean(RedisConnectionDetails.class); + assertThat(connectionDetails.getClass().getName()).isEqualTo(REDIS_CONTAINER_CONNECTION_DETAILS); + } + } + + @Test + void whenHasExistingAutoConfigurationRegistersReplacement() { + try (AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext()) { + applicationContext.register(WithRedisAutoConfiguration.class, ContainerConfiguration.class); + new TestcontainersLifecycleApplicationContextInitializer().initialize(applicationContext); + applicationContext.refresh(); + RedisConnectionDetails connectionDetails = applicationContext.getBean(RedisConnectionDetails.class); + assertThat(connectionDetails.getClass().getName()).isEqualTo(REDIS_CONTAINER_CONNECTION_DETAILS); + } + } + + @Test + void whenHasUserConfigurationDoesNotRegisterReplacement() { + try (AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext()) { + applicationContext.register(UserConfiguration.class, WithRedisAutoConfiguration.class, + ContainerConfiguration.class); + new TestcontainersLifecycleApplicationContextInitializer().initialize(applicationContext); + applicationContext.refresh(); + RedisConnectionDetails connectionDetails = applicationContext.getBean(RedisConnectionDetails.class); + assertThat(Mockito.mockingDetails(connectionDetails).isMock()).isTrue(); + } + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(ServiceConnectionAutoConfiguration.class) + static class WithNoExtraAutoConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration({ ServiceConnectionAutoConfiguration.class, RedisAutoConfiguration.class }) + static class WithRedisAutoConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + static class ContainerConfiguration { + + @Bean + @ServiceConnection + RedisContainer redisContainer() { + return new RedisContainer(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class UserConfiguration { + + @Bean + RedisConnectionDetails redisConnectionDetails() { + return mock(RedisConnectionDetails.class); + } + + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-session-redis/src/test/java/smoketest/session/redis/TestSampleSessionRedisApplication.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-session-redis/src/test/java/smoketest/session/redis/TestSampleSessionRedisApplication.java new file mode 100644 index 0000000000..d321d24a19 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-session-redis/src/test/java/smoketest/session/redis/TestSampleSessionRedisApplication.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2023 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 smoketest.session.redis; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.testcontainers.RedisContainer; +import org.springframework.context.annotation.Bean; + +@TestConfiguration(proxyBeanMethods = false) +public class TestSampleSessionRedisApplication { + + @Bean + @ServiceConnection + RedisContainer redisContainer() { + return new RedisContainer(); + } + + public static void main(String[] args) { + SpringApplication.from(SampleSessionRedisApplication::main) + .with(TestSampleSessionRedisApplication.class) + .run(args); + } + +}