Skip to content

Commit 58daa40

Browse files
committed
ImportTestcontainers doesn't work with AOT
Introduce `TestcontainersBeanRegistrationAotProcessor` which replaces the `InstanceSupplier` of a `Container` with either direct field access or an equivalent reflection-based approach. Add `DynamicPropertySourceMethodsBeanFactoryInitializationAotProcessor` which generates methods for each method annotated with `@DynamicPropertySource`. Signed-off-by: Dmytro Nosan <[email protected]>
1 parent 3a6e4e9 commit 58daa40

File tree

6 files changed

+522
-6
lines changed

6 files changed

+522
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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+
@CompileWithForkedClassLoader
54+
class ImportTestcontainersAotTests {
55+
56+
private final TestGenerationContext generationContext = new TestGenerationContext();
57+
58+
private final AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
59+
60+
@AfterEach
61+
void teardown() {
62+
this.applicationContext.close();
63+
}
64+
65+
@Test
66+
void importTestcontainersImportWithoutValue() {
67+
this.applicationContext.register(ImportWithoutValue.class);
68+
compile((aotApplicationContext, compiled) -> {
69+
PostgreSQLContainer<?> container = aotApplicationContext.getBean(PostgreSQLContainer.class);
70+
assertThat(container).isSameAs(ImportWithoutValue.container);
71+
});
72+
}
73+
74+
@Test
75+
void importTestcontainersImportWithoutValueWithDynamicPropertySource() {
76+
this.applicationContext.register(ImportWithoutValueWithDynamicPropertySource.class);
77+
compile((aotApplicationContext, compiled) -> {
78+
PostgreSQLContainer<?> container = aotApplicationContext.getBean(PostgreSQLContainer.class);
79+
assertThat(container).isSameAs(ImportWithoutValueWithDynamicPropertySource.container);
80+
assertThat(aotApplicationContext.getEnvironment().getProperty("container.port", Integer.class))
81+
.isEqualTo(ImportWithoutValueWithDynamicPropertySource.container.getFirstMappedPort());
82+
});
83+
}
84+
85+
@Test
86+
void importTestcontainersCustomPostgreSQLContainerDefinitions() {
87+
this.applicationContext.register(CustomPostgresqlContainerDefinitions.class);
88+
compile((aotApplicationContext, compiled) -> {
89+
CustomPostgreSQLContainer container = aotApplicationContext.getBean(CustomPostgreSQLContainer.class);
90+
assertThat(container).isSameAs(CustomPostgresqlContainerDefinitions.container);
91+
});
92+
}
93+
94+
@Test
95+
void importTestcontainersImportWithoutValueNotAccessibleContainerAndDynamicPropertySource() {
96+
this.applicationContext.register(ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.class);
97+
compile((aotApplicationContext, compiled) -> {
98+
MongoDBContainer container = aotApplicationContext.getBean(MongoDBContainer.class);
99+
assertThat(container).isSameAs(ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.container);
100+
assertThat(aotApplicationContext.getEnvironment().getProperty("mongo.port", Integer.class)).isEqualTo(
101+
ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.container.getFirstMappedPort());
102+
});
103+
}
104+
105+
@Test
106+
void importTestcontainersWithNotAccessibleContainerAndDynamicPropertySource() {
107+
this.applicationContext.register(ImportWithValueAndDynamicPropertySource.class);
108+
compile((aotApplicationContext, compiled) -> {
109+
PostgreSQLContainer<?> container = aotApplicationContext.getBean(PostgreSQLContainer.class);
110+
assertThat(container).isSameAs(ContainerDefinitionsWithDynamicPropertySource.container);
111+
assertThat(aotApplicationContext.getEnvironment().getProperty("postgres.port", Integer.class))
112+
.isEqualTo(ContainerDefinitionsWithDynamicPropertySource.container.getFirstMappedPort());
113+
});
114+
}
115+
116+
@Test
117+
void importTestcontainersMultipleContainersAndDynamicPropertySources() {
118+
this.applicationContext.register(ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.class);
119+
this.applicationContext.register(ImportWithValueAndDynamicPropertySource.class);
120+
compile((aotApplicationContext, compiled) -> {
121+
MongoDBContainer mongo = aotApplicationContext.getBean(MongoDBContainer.class);
122+
PostgreSQLContainer<?> postgres = aotApplicationContext.getBean(PostgreSQLContainer.class);
123+
assertThat(mongo).isSameAs(ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.container);
124+
assertThat(postgres).isSameAs(ContainerDefinitionsWithDynamicPropertySource.container);
125+
ConfigurableEnvironment environment = aotApplicationContext.getEnvironment();
126+
assertThat(environment.getProperty("postgres.port", Integer.class))
127+
.isEqualTo(ContainerDefinitionsWithDynamicPropertySource.container.getFirstMappedPort());
128+
assertThat(environment.getProperty("mongo.port", Integer.class)).isEqualTo(
129+
ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.container.getFirstMappedPort());
130+
});
131+
}
132+
133+
@SuppressWarnings("unchecked")
134+
private void compile(BiConsumer<GenericApplicationContext, Compiled> result) {
135+
ClassName className = processAheadOfTime();
136+
TestCompiler.forSystem().with(this.generationContext).compile((compiled) -> {
137+
try (GenericApplicationContext aotApplicationContext = new GenericApplicationContext()) {
138+
new TestcontainersLifecycleApplicationContextInitializer().initialize(aotApplicationContext);
139+
ApplicationContextInitializer<GenericApplicationContext> initializer = compiled
140+
.getInstance(ApplicationContextInitializer.class, className.toString());
141+
initializer.initialize(aotApplicationContext);
142+
aotApplicationContext.refresh();
143+
result.accept(aotApplicationContext, compiled);
144+
}
145+
});
146+
}
147+
148+
private ClassName processAheadOfTime() {
149+
ClassName className = new ApplicationContextAotGenerator().processAheadOfTime(this.applicationContext,
150+
this.generationContext);
151+
this.generationContext.writeGeneratedContent();
152+
return className;
153+
}
154+
155+
@ImportTestcontainers
156+
static class ImportWithoutValue {
157+
158+
@ContainerAnnotation
159+
static PostgreSQLContainer<?> container = TestImage.container(PostgreSQLContainer.class);
160+
161+
}
162+
163+
@ImportTestcontainers(ContainerDefinitions.class)
164+
static class ImportWithValue {
165+
166+
}
167+
168+
static class ContainerDefinitions {
169+
170+
@ContainerAnnotation
171+
PostgreSQLContainer<?> container = TestImage.container(PostgreSQLContainer.class);
172+
173+
}
174+
175+
private interface ContainerDefinitionsWithDynamicPropertySource {
176+
177+
@ContainerAnnotation
178+
PostgreSQLContainer<?> container = TestImage.container(PostgreSQLContainer.class);
179+
180+
@DynamicPropertySource
181+
static void containerProperties(DynamicPropertyRegistry registry) {
182+
registry.add("postgres.port", container::getFirstMappedPort);
183+
}
184+
185+
}
186+
187+
@Retention(RetentionPolicy.RUNTIME)
188+
@interface ContainerAnnotation {
189+
190+
}
191+
192+
@ImportTestcontainers
193+
static class ImportWithoutValueWithDynamicPropertySource {
194+
195+
static PostgreSQLContainer<?> container = TestImage.container(PostgreSQLContainer.class);
196+
197+
@DynamicPropertySource
198+
static void containerProperties(DynamicPropertyRegistry registry) {
199+
registry.add("container.port", container::getFirstMappedPort);
200+
}
201+
202+
}
203+
204+
@ImportTestcontainers
205+
static class CustomPostgresqlContainerDefinitions {
206+
207+
private static final CustomPostgreSQLContainer container = new CustomPostgreSQLContainer();
208+
209+
}
210+
211+
static class CustomPostgreSQLContainer extends PostgreSQLContainer<CustomPostgreSQLContainer> {
212+
213+
CustomPostgreSQLContainer() {
214+
super("postgres:14");
215+
}
216+
217+
}
218+
219+
@ImportTestcontainers
220+
static class ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource {
221+
222+
private static final MongoDBContainer container = TestImage.container(MongoDBContainer.class);
223+
224+
@DynamicPropertySource
225+
private static void containerProperties(DynamicPropertyRegistry registry) {
226+
registry.add("mongo.port", container::getFirstMappedPort);
227+
}
228+
229+
}
230+
231+
@ImportTestcontainers(ContainerDefinitionsWithDynamicPropertySource.class)
232+
static class ImportWithValueAndDynamicPropertySource {
233+
234+
}
235+
236+
}

0 commit comments

Comments
 (0)