Skip to content

Commit

Permalink
TestcontainersBeanRegistrationAotProcessor that replaces InstanceSupp…
Browse files Browse the repository at this point in the history
…lier of Container by either direct field usage or a reflection equivalent.

If the field is private, the reflection will be used; otherwise, direct access to the field will be used

DynamicPropertySourceBeanFactoryInitializationAotProcessor that generates methods for each annotated @DynamicPropertySource method
  • Loading branch information
nosan committed Oct 28, 2024
1 parent 4718485 commit 65e3548
Show file tree
Hide file tree
Showing 5 changed files with 464 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,29 @@

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.function.BiConsumer;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.Container;
import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.containers.PostgreSQLContainer;

import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.boot.testcontainers.beans.TestcontainerBeanDefinition;
import org.springframework.boot.testcontainers.context.ImportTestcontainers;
import org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleApplicationContextInitializer;
import org.springframework.boot.testsupport.container.DisabledIfDockerUnavailable;
import org.springframework.boot.testsupport.container.TestImage;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.aot.ApplicationContextAotGenerator;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.test.tools.CompileWithForkedClassLoader;
import org.springframework.core.test.tools.Compiled;
import org.springframework.core.test.tools.TestCompiler;
import org.springframework.javapoet.ClassName;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;

Expand All @@ -43,6 +55,8 @@
@DisabledIfDockerUnavailable
class ImportTestcontainersTests {

private final TestGenerationContext generationContext = new TestGenerationContext();

private AnnotationConfigApplicationContext applicationContext;

@AfterEach
Expand Down Expand Up @@ -102,7 +116,7 @@ void importWhenHasNonStaticContainerFieldThrowsException() {
@Test
void importWhenHasContainerDefinitionsWithDynamicPropertySource() {
this.applicationContext = new AnnotationConfigApplicationContext(
ContainerDefinitionsWithDynamicPropertySource.class);
ImportWithoutValueWithDynamicPropertySource.class);
assertThat(this.applicationContext.getEnvironment().containsProperty("container.port")).isTrue();
}

Expand All @@ -122,6 +136,119 @@ void importWhenHasBadArgsDynamicPropertySourceMethod() {
.withMessage("@DynamicPropertySource method 'containerProperties' must be static");
}

@Test
@CompileWithForkedClassLoader
void importTestcontainersImportWithoutValueAotContribution() {
this.applicationContext = new AnnotationConfigApplicationContext();
this.applicationContext.register(ImportWithoutValue.class);
compile((freshContext, compiled) -> {
PostgreSQLContainer<?> container = freshContext.getBean(PostgreSQLContainer.class);
assertThat(container).isSameAs(ImportWithoutValue.container);
});
}

@Test
@CompileWithForkedClassLoader
void importTestcontainersImportWithValueAotContribution() {
this.applicationContext = new AnnotationConfigApplicationContext();
this.applicationContext.register(ImportWithValue.class);
compile((freshContext, compiled) -> {
PostgreSQLContainer<?> container = freshContext.getBean(PostgreSQLContainer.class);
assertThat(container).isSameAs(ContainerDefinitions.container);
});
}

@Test
@CompileWithForkedClassLoader
void importTestcontainersImportWithoutValueWithDynamicPropertySourceAotContribution() {
this.applicationContext = new AnnotationConfigApplicationContext();
this.applicationContext.register(ImportWithoutValueWithDynamicPropertySource.class);
compile((freshContext, compiled) -> {
PostgreSQLContainer<?> container = freshContext.getBean(PostgreSQLContainer.class);
assertThat(container).isSameAs(ImportWithoutValueWithDynamicPropertySource.container);
assertThat(freshContext.getEnvironment().getProperty("container.port", Integer.class))
.isEqualTo(ImportWithoutValueWithDynamicPropertySource.container.getFirstMappedPort());
});
}

@Test
@CompileWithForkedClassLoader
void importTestcontainersCustomPostgreSQLContainerDefinitionsAotContribution() {
this.applicationContext = new AnnotationConfigApplicationContext();
this.applicationContext.register(CustomPostgreSQLContainerDefinitions.class);
compile((freshContext, compiled) -> {
CustomPostgreSQLContainer container = freshContext.getBean(CustomPostgreSQLContainer.class);
assertThat(container).isSameAs(CustomPostgreSQLContainerDefinitions.container);
});
}

@Test
@CompileWithForkedClassLoader
void importTestcontainersImportWithoutValueNotAccessibleContainerAndDynamicPropertySourceAotContribution() {
this.applicationContext = new AnnotationConfigApplicationContext();
this.applicationContext.register(ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.class);
compile((freshContext, compiled) -> {
MongoDBContainer container = freshContext.getBean(MongoDBContainer.class);
assertThat(container).isSameAs(ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.container);
assertThat(freshContext.getEnvironment().getProperty("mongo.port", Integer.class)).isEqualTo(
ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.container.getFirstMappedPort());
});
}

@Test
@CompileWithForkedClassLoader
void importTestcontainersWithNotAccessibleContainerAndDynamicPropertySourceAotContribution() {
this.applicationContext = new AnnotationConfigApplicationContext();
this.applicationContext.register(ImportWithValueAndDynamicPropertySource.class);
compile((freshContext, compiled) -> {
PostgreSQLContainer<?> container = freshContext.getBean(PostgreSQLContainer.class);
assertThat(container).isSameAs(ContainerDefinitionsWithDynamicPropertySource.container);
assertThat(freshContext.getEnvironment().getProperty("postgres.port", Integer.class))
.isEqualTo(ContainerDefinitionsWithDynamicPropertySource.container.getFirstMappedPort());
});
}

@Test
@CompileWithForkedClassLoader
void importTestcontainersMultipleContainersAndDynamicPropertySourcesAotContribution() {
this.applicationContext = new AnnotationConfigApplicationContext();
this.applicationContext.register(ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.class);
this.applicationContext.register(ImportWithValueAndDynamicPropertySource.class);
compile((freshContext, compiled) -> {
MongoDBContainer mongo = freshContext.getBean(MongoDBContainer.class);
PostgreSQLContainer<?> postgres = freshContext.getBean(PostgreSQLContainer.class);
assertThat(mongo).isSameAs(ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.container);
assertThat(postgres).isSameAs(ContainerDefinitionsWithDynamicPropertySource.container);
ConfigurableEnvironment environment = freshContext.getEnvironment();
assertThat(environment.getProperty("postgres.port", Integer.class))
.isEqualTo(ContainerDefinitionsWithDynamicPropertySource.container.getFirstMappedPort());
assertThat(environment.getProperty("mongo.port", Integer.class)).isEqualTo(
ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource.container.getFirstMappedPort());
});
}

@SuppressWarnings("unchecked")
private void compile(BiConsumer<GenericApplicationContext, Compiled> result) {
ClassName className = processAheadOfTime();
TestCompiler.forSystem().with(this.generationContext).compile((compiled) -> {
try (GenericApplicationContext context = new GenericApplicationContext()) {
new TestcontainersLifecycleApplicationContextInitializer().initialize(context);
ApplicationContextInitializer<GenericApplicationContext> initializer = compiled
.getInstance(ApplicationContextInitializer.class, className.toString());
initializer.initialize(context);
context.refresh();
result.accept(context, compiled);
}
});
}

private ClassName processAheadOfTime() {
ClassName className = new ApplicationContextAotGenerator().processAheadOfTime(this.applicationContext,
this.generationContext);
this.generationContext.writeGeneratedContent();
return className;
}

@ImportTestcontainers
static class ImportWithoutValue {

Expand Down Expand Up @@ -161,13 +288,25 @@ interface ContainerDefinitions {

}

private interface ContainerDefinitionsWithDynamicPropertySource {

@ContainerAnnotation
PostgreSQLContainer<?> container = TestImage.container(PostgreSQLContainer.class);

@DynamicPropertySource
static void containerProperties(DynamicPropertyRegistry registry) {
registry.add("postgres.port", container::getFirstMappedPort);
}

}

@Retention(RetentionPolicy.RUNTIME)
@interface ContainerAnnotation {

}

@ImportTestcontainers
static class ContainerDefinitionsWithDynamicPropertySource {
static class ImportWithoutValueWithDynamicPropertySource {

static PostgreSQLContainer<?> container = TestImage.container(PostgreSQLContainer.class);

Expand Down Expand Up @@ -196,4 +335,36 @@ void containerProperties() {

}

@ImportTestcontainers
static class CustomPostgreSQLContainerDefinitions {

private static final CustomPostgreSQLContainer container = new CustomPostgreSQLContainer();

}

static class CustomPostgreSQLContainer extends PostgreSQLContainer<CustomPostgreSQLContainer> {

CustomPostgreSQLContainer() {
super("postgres:14");
}

}

@ImportTestcontainers
static class ImportWithoutValueNotAccessibleContainerAndDynamicPropertySource {

private static final MongoDBContainer container = TestImage.container(MongoDBContainer.class);

@DynamicPropertySource
private static void containerProperties(DynamicPropertyRegistry registry) {
registry.add("mongo.port", container::getFirstMappedPort);
}

}

@ImportTestcontainers(ContainerDefinitionsWithDynamicPropertySource.class)
static class ImportWithValueAndDynamicPropertySource {

}

}
Loading

0 comments on commit 65e3548

Please sign in to comment.