Skip to content

Commit b39e937

Browse files
committed
ImportTestcontainers doesn't work with AOT
Add TestcontainersBeanRegistrationAotProcessor that replaces InstanceSupplier of Container by either direct field usage or a reflection equivalent. Add DynamicPropertySourceBeanFactoryInitializationAotProcessor that generates methods for each annotated @DynamicPropertySource method See gh-42891 Signed-off-by: Dmytro Nosan <[email protected]>
1 parent 15b63fa commit b39e937

File tree

5 files changed

+530
-5
lines changed

5 files changed

+530
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.testcontainers;
18+
19+
import java.lang.annotation.Retention;
20+
import java.lang.annotation.RetentionPolicy;
21+
import java.util.function.BiConsumer;
22+
23+
import org.junit.jupiter.api.AfterEach;
24+
import org.junit.jupiter.api.Test;
25+
import org.testcontainers.containers.MongoDBContainer;
26+
import org.testcontainers.containers.PostgreSQLContainer;
27+
28+
import org.springframework.aot.test.generate.TestGenerationContext;
29+
import org.springframework.boot.testcontainers.context.ImportTestcontainers;
30+
import org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleApplicationContextInitializer;
31+
import org.springframework.boot.testsupport.container.DisabledIfDockerUnavailable;
32+
import org.springframework.boot.testsupport.container.TestImage;
33+
import org.springframework.context.ApplicationContextInitializer;
34+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
35+
import org.springframework.context.aot.ApplicationContextAotGenerator;
36+
import org.springframework.context.support.GenericApplicationContext;
37+
import org.springframework.core.env.ConfigurableEnvironment;
38+
import org.springframework.core.test.tools.CompileWithForkedClassLoader;
39+
import org.springframework.core.test.tools.Compiled;
40+
import org.springframework.core.test.tools.TestCompiler;
41+
import org.springframework.javapoet.ClassName;
42+
import org.springframework.test.context.DynamicPropertyRegistry;
43+
import org.springframework.test.context.DynamicPropertySource;
44+
45+
import static org.assertj.core.api.Assertions.assertThat;
46+
47+
/**
48+
* AoT Tests for {@link ImportTestcontainers}.
49+
*
50+
* @author Dmytro Nosan
51+
*/
52+
@DisabledIfDockerUnavailable
53+
class ImportTestcontainersAotTests {
54+
55+
private final TestGenerationContext generationContext = new TestGenerationContext();
56+
57+
private final AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
58+
59+
@AfterEach
60+
void teardown() {
61+
this.applicationContext.close();
62+
}
63+
64+
@Test
65+
@CompileWithForkedClassLoader
66+
void importTestcontainersImportWithoutValue() {
67+
this.applicationContext.register(ImportWithoutValue.class);
68+
compile((freshContext, compiled) -> {
69+
PostgreSQLContainer<?> container = freshContext.getBean(PostgreSQLContainer.class);
70+
assertThat(container).isSameAs(ImportWithoutValue.container);
71+
});
72+
}
73+
74+
@Test
75+
@CompileWithForkedClassLoader
76+
void importTestcontainersImportWithoutValueWithDynamicPropertySource() {
77+
this.applicationContext.register(ImportWithoutValueWithDynamicPropertySource.class);
78+
compile((freshContext, compiled) -> {
79+
PostgreSQLContainer<?> container = freshContext.getBean(PostgreSQLContainer.class);
80+
assertThat(container).isSameAs(ImportWithoutValueWithDynamicPropertySource.container);
81+
assertThat(freshContext.getEnvironment().getProperty("container.port", Integer.class))
82+
.isEqualTo(ImportWithoutValueWithDynamicPropertySource.container.getFirstMappedPort());
83+
});
84+
}
85+
86+
@Test
87+
@CompileWithForkedClassLoader
88+
void importTestcontainersCustomPostgreSQLContainerDefinitions() {
89+
this.applicationContext.register(CustomPostgresqlContainerDefinitions.class);
90+
compile((freshContext, compiled) -> {
91+
CustomPostgreSQLContainer container = freshContext.getBean(CustomPostgreSQLContainer.class);
92+
assertThat(container).isSameAs(CustomPostgresqlContainerDefinitions.container);
93+
});
94+
}
95+
96+
@Test
97+
@CompileWithForkedClassLoader
98+
void importTestcontainersImportWithoutValueNotAccessibleContainerAndDynamicPropertySource() {
99+
this.applicationContext.register(ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.class);
100+
compile((freshContext, compiled) -> {
101+
MongoDBContainer container = freshContext.getBean(MongoDBContainer.class);
102+
assertThat(container).isSameAs(ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.container);
103+
assertThat(freshContext.getEnvironment().getProperty("mongo.port", Integer.class)).isEqualTo(
104+
ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.container.getFirstMappedPort());
105+
});
106+
}
107+
108+
@Test
109+
@CompileWithForkedClassLoader
110+
void importTestcontainersWithNotAccessibleContainerAndDynamicPropertySource() {
111+
this.applicationContext.register(ImportWithValueAndDynamicPropertySource.class);
112+
compile((freshContext, compiled) -> {
113+
PostgreSQLContainer<?> container = freshContext.getBean(PostgreSQLContainer.class);
114+
assertThat(container).isSameAs(ContainerDefinitionsWithDynamicPropertySource.container);
115+
assertThat(freshContext.getEnvironment().getProperty("postgres.port", Integer.class))
116+
.isEqualTo(ContainerDefinitionsWithDynamicPropertySource.container.getFirstMappedPort());
117+
});
118+
}
119+
120+
@Test
121+
@CompileWithForkedClassLoader
122+
void importTestcontainersMultipleContainersAndDynamicPropertySources() {
123+
this.applicationContext.register(ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.class);
124+
this.applicationContext.register(ImportWithValueAndDynamicPropertySource.class);
125+
compile((freshContext, compiled) -> {
126+
MongoDBContainer mongo = freshContext.getBean(MongoDBContainer.class);
127+
PostgreSQLContainer<?> postgres = freshContext.getBean(PostgreSQLContainer.class);
128+
assertThat(mongo).isSameAs(ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.container);
129+
assertThat(postgres).isSameAs(ContainerDefinitionsWithDynamicPropertySource.container);
130+
ConfigurableEnvironment environment = freshContext.getEnvironment();
131+
assertThat(environment.getProperty("postgres.port", Integer.class))
132+
.isEqualTo(ContainerDefinitionsWithDynamicPropertySource.container.getFirstMappedPort());
133+
assertThat(environment.getProperty("mongo.port", Integer.class)).isEqualTo(
134+
ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.container.getFirstMappedPort());
135+
});
136+
}
137+
138+
@SuppressWarnings("unchecked")
139+
private void compile(BiConsumer<GenericApplicationContext, Compiled> result) {
140+
ClassName className = processAheadOfTime();
141+
TestCompiler.forSystem().with(this.generationContext).compile((compiled) -> {
142+
try (GenericApplicationContext context = new GenericApplicationContext()) {
143+
new TestcontainersLifecycleApplicationContextInitializer().initialize(context);
144+
ApplicationContextInitializer<GenericApplicationContext> initializer = compiled
145+
.getInstance(ApplicationContextInitializer.class, className.toString());
146+
initializer.initialize(context);
147+
context.refresh();
148+
result.accept(context, compiled);
149+
}
150+
});
151+
}
152+
153+
private ClassName processAheadOfTime() {
154+
ClassName className = new ApplicationContextAotGenerator().processAheadOfTime(this.applicationContext,
155+
this.generationContext);
156+
this.generationContext.writeGeneratedContent();
157+
return className;
158+
}
159+
160+
@ImportTestcontainers
161+
static class ImportWithoutValue {
162+
163+
@ContainerAnnotation
164+
static PostgreSQLContainer<?> container = TestImage.container(PostgreSQLContainer.class);
165+
166+
}
167+
168+
@ImportTestcontainers(ContainerDefinitions.class)
169+
static class ImportWithValue {
170+
171+
}
172+
173+
static class ContainerDefinitions {
174+
175+
@ContainerAnnotation
176+
PostgreSQLContainer<?> container = TestImage.container(PostgreSQLContainer.class);
177+
178+
}
179+
180+
private interface ContainerDefinitionsWithDynamicPropertySource {
181+
182+
@ContainerAnnotation
183+
PostgreSQLContainer<?> container = TestImage.container(PostgreSQLContainer.class);
184+
185+
@DynamicPropertySource
186+
static void containerProperties(DynamicPropertyRegistry registry) {
187+
registry.add("postgres.port", container::getFirstMappedPort);
188+
}
189+
190+
}
191+
192+
@Retention(RetentionPolicy.RUNTIME)
193+
@interface ContainerAnnotation {
194+
195+
}
196+
197+
@ImportTestcontainers
198+
static class ImportWithoutValueWithDynamicPropertySource {
199+
200+
static PostgreSQLContainer<?> container = TestImage.container(PostgreSQLContainer.class);
201+
202+
@DynamicPropertySource
203+
static void containerProperties(DynamicPropertyRegistry registry) {
204+
registry.add("container.port", container::getFirstMappedPort);
205+
}
206+
207+
}
208+
209+
@ImportTestcontainers
210+
static class CustomPostgresqlContainerDefinitions {
211+
212+
private static final CustomPostgreSQLContainer container = new CustomPostgreSQLContainer();
213+
214+
}
215+
216+
static class CustomPostgreSQLContainer extends PostgreSQLContainer<CustomPostgreSQLContainer> {
217+
218+
CustomPostgreSQLContainer() {
219+
super("postgres:14");
220+
}
221+
222+
}
223+
224+
@ImportTestcontainers
225+
static class ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource {
226+
227+
private static final MongoDBContainer container = TestImage.container(MongoDBContainer.class);
228+
229+
@DynamicPropertySource
230+
private static void containerProperties(DynamicPropertyRegistry registry) {
231+
registry.add("mongo.port", container::getFirstMappedPort);
232+
}
233+
234+
}
235+
236+
@ImportTestcontainers(ContainerDefinitionsWithDynamicPropertySource.class)
237+
static class ImportWithValueAndDynamicPropertySource {
238+
239+
}
240+
241+
}

0 commit comments

Comments
 (0)