testcontainers-java
testcontainers-java copied to clipboard
Spring Boot Tests with JUnit5 Jupiter and Shared Containers
Currently, a typical test setup with Spring Boot tests and shared containers does not work anymore with JUnit 5: Using static testcontainers with Spring Boot's ApplicationContextInitializer
to configure the server's ports in the Spring context.
The problem is that the Spring application context gets initialized during Jupiter extension's post processing while testcontainers are started during before all callbacks. The latter happens after post processing.
See https://github.com/junit-team/junit5/blob/c9ae6e261550481362f17d21e88137a8c71fc7c8/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java#L184 vs. https://github.com/junit-team/junit5/blob/c9ae6e261550481362f17d21e88137a8c71fc7c8/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java#L189
One approach to solve this could be to start the testcontainers also during post processing (TestInstancePostProcesso#postProcessTestInstancer
)
Then the only remaining problem would be to ensure that the TestcontainersExtension
always gets created before the SpringExtension
.
Problematic Example
@SpringBootTest
@ContextConfiguration(initializers = {Initializer.class})
@Testcontainers
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class JupiterSharedTestcontainersTests {
@Container
public static JdbcDatabaseContainer postgreSQLContainer = new PostgreSQLContainer();
@Test
public void testTomatoes() {
assertTrue("tomatoes".equals("tomatoes"));
}
public static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
TestPropertyValues.of(
"spring.datasource.url=jdbc:postgresql://"
+ postgreSQLContainer.getContainerIpAddress()
+ ":" + postgreSQLContainer.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT)
+ "/db_name"
).applyTo(configurableApplicationContext.getEnvironment());
}
}
}
What do you think?
Hi, did you try to put @TestInstance(TestInstance.Lifecycle.PER_CLASS)
annotation on your test class?
This should allow you to create BeforeAll/AfterAll(which is actually used by testcontainers extension) as non-static methods.
Hi, yes I did. That is exactly how I came accross this problem.
I updated the initial request with an example that shows the problem.
Two possible workarounds I can think of:
- [if makes sense] Use the Singleton Container Pattern.
- Explicitly start the container in 'initialize' and make it a reusable container as explained here if applicable (will not currently work for a docker-compose container for instance).
it doesn't have to be marked as reusable if you only need to use the same container between the tests. Reusable Containers feature is for reusing containers between the test runs (think running mvn test
twice)
@KaiStapel you can remove @Container
from postgreSQLContainer
and call postgreSQLContainer.start()
in Initializer
.
Also, in your particular example, you don't need all of it, just use the JDBC URL support: https://www.testcontainers.org/modules/databases/#database-containers-launched-via-jdbc-url-scheme
@fullkomnun and @bsideup Thanks for proposing these workarounds. We were already using the singleton container pattern to make it work.
Still, imo, this ticket makes sense to fix the TestcontainersExtension to work for shared containers with JUnit5 or at least mention its limitations in the documentation.
Hi all. I would support @KaiStapel's position here. I ran down his same path trying to add @TestInstance(TestInstance.Lifecycle.PER_CLASS)
to class this morning. The setup is somewhat expensive. It would be nice to execute it once.
The order dependence between the extensions is a big down side, but I don't see an easy way around that.
Can you try https://spring.io/blog/2020/03/27/dynamicpropertysource-in-spring-framework-5-2-5-and-spring-boot-2-2-6 ?
@bsideup I tried your suggestion and still get the same exception. The test continues and refreshes the context after waiting for db to be ready, so it succeeds. But it would be nice if the context were initialized for the first time only once the db is ready.
@yanivnahoum
But it would be nice if the context were initialized for the first time only once the db is ready.
That's how it works already. Please check your setup (and share a reproducer otherwise)
Thanks @bsideup, here's an example using Spring Boot 2.3.0 & Testcontainers 1.14.2
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.env.Environment;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SpringBootTest
@Testcontainers
class MySqlTest {
@Container
private static final MySQLContainer<?> mySqlContainer = new MySQLContainer<>("mysql:8.0.20");
static {
// mySqlContainer.start();
}
@DynamicPropertySource
static void mySqlProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mySqlContainer::getJdbcUrl);
registry.add("spring.datasource.username", mySqlContainer::getUsername);
registry.add("spring.datasource.password", mySqlContainer::getPassword);
}
@Test
void test(@Autowired Environment env) {
System.out.println("#test - spring.datasource.url=" + env.getProperty("spring.datasource.url"));
}
}
This results in an exception with the following root cause:
java.lang.IllegalStateException: Mapped port can only be obtained after the container is started
at org.testcontainers.shaded.com.google.common.base.Preconditions.checkState(Preconditions.java:174)
at org.testcontainers.containers.ContainerState.getMappedPort(ContainerState.java:141)
at org.testcontainers.containers.MySQLContainer.getJdbcUrl(MySQLContainer.java:73)
at org.springframework.test.context.support.DynamicValuesPropertySource.getProperty(DynamicValuesPropertySource.java:43)
at org.springframework.core.env.PropertySourcesPropertyResolver.getProperty(PropertySourcesPropertyResolver.java:85)
at org.springframework.core.env.PropertySourcesPropertyResolver.getProperty(PropertySourcesPropertyResolver.java:62)
at org.springframework.core.env.AbstractEnvironment.getProperty(AbstractEnvironment.java:535)
at org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration$EmbeddedDatabaseCondition.hasDataSourceUrlProperty(DataSourceAutoConfiguration.java:147)
at org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration$EmbeddedDatabaseCondition.getMatchOutcome(DataSourceAutoConfiguration.java:130)
at org.springframework.boot.autoconfigure.condition.SpringBootCondition.matches(SpringBootCondition.java:47)
after which the context loads again, the properties are set correctly and the test passes
@yanivnahoum please share (or update) full example (with imports).
@bsideup I edited the comment above
@yanivnahoum I just tried your example and got a successful result:
#test - spring.datasource.url=jdbc:postgresql://localhost:33492/test?loggerLevel=OFF
Have you tried running it? Can you share a full project that reproduces the issue?
@bsideup you switched to Postgres? Anyway, in my example, the test also succeeds and prints the url, but only after the exception stated above occurs. Does the same happen to you?
@yanivnahoum I did (I only had postgres module on classpath), but it does not change anything
Anyway, in my example, the test also succeeds and prints the url, but only after the exception stated above occurs. Does the same happen to you?
No, I did not get any exception in the logs.
@bsideup then I'll create a repo with just that example and share it
Please note that this ticket is only about the problem when adding @TestInstance(TestInstance.Lifecycle.PER_CLASS)
on the test class. Without it everything seems to work fine and as expected (including only starting the container once).
While trying to create a minimal example to reproduce the issue, I found the culprit: org.springframework.cloud.contract.wiremock.WireMockTestExecutionListener
from spring-cloud-contract-wiremock. It forces the application context to load before the tc db is ready....
@bsideup thanks for your help!
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If you believe this is a mistake, please reply to this comment to keep it open. If there isn't one already, a PR to fix or at least reproduce the problem in a test case will always help us get back on track to tackle this.
This is an important issue - any chance it might have been fixed or addressed in a more recent release I am seeing it with SpringBoot 2.3.0 TestContainers 1.14.3
using both
-
PostgreSQLContainer
-
DockerComposeContainer
@sbrannen can it be an issue with Spring Boot's Lifecycle and not Testcontainers' extension?
Any updates on this ?
I had a similar problem with a JUnit 5 extension I wrote and I found out when tests are run in TestInstance.Lifecycle.PER_CLASS
Spring's application context will be loaded before the org.junit.jupiter.api.extension.BeforeAllCallback#beforeAll
is called. So I solved the problem by moving my logic to org.junit.jupiter.api.extension.TestInstancePostProcessor#postProcessTestInstance
.
Does it also make sense to start the "Shared Containers" on postProcessTestInstance
?
I would say this needs to be solved with a TestExecutionListener
Create a Spring Boot Starter spring-boot-starter-testcontainers
with the Following dependencies:
- org.testcontainers:testcontainers
- org.testcontainers:junit-jupiter
Where the TestContainersTestExecutionListener
needs to have something like this:
@Override
public void beforeTestClass(final TestContext testContext) throws Exception {
super.beforeTestClass(testContext);
Arrays.stream(testContext.getTestClass().getFields())
.filter(
Predicate.not(field -> Objects.isNull(field.getAnnotation(Container.class))))
.filter(field -> field.getType().isAssignableFrom(org.testcontainers.containers.Container.class))
.forEach(field -> startMethod.invoke(field));
}
And introduce something like @AutoConfigureTestContainers
I think that'd be a spring-specific solution. There's a already a junit 5 annotation @Testcontainers
which is loading TestContainer's junit extension TestcontainersExtension
and this extension is doing the handling of containers. So if this extension is 'fixed' then TestContainers will remain spring independent
@bsideup your thoughts?
Used an extension coupled with a context customizer:
public class DatabaseExtension implements BeforeAllCallback, AfterAllCallback {
private static final ThreadLocal<CustomContainer> THREAD_CONTAINER = new ThreadLocal<>();
@Override
public void beforeAll(ExtensionContext context) {
// This can come from a custom extension, or a hard coded default if that makes sense in your project
String imageName = "";
// Custom container can be a vanilla MySQLContainer, or something custom if you need a specific JDBC URL, etc
CustomContainer container = new CustomContainer(DockerImageName.parse(imageName).asCompatibleSubstituteFor(MySQLContainer.NAME));
THREAD_CONTAINER.set(container);
container.start();
}
@Override
public void afterAll(ExtensionContext context) {
CustomContainer container = THREAD_CONTAINER.get();
if (container != null) {
// You should clear the thread local at this stage
container.stop();
}
}
// This class should be added to META-INF/spring.factories
public static class CustomizerFactory implements ContextCustomizerFactory {
@Override
public ContextCustomizer createContextCustomizer(Class<?> testClass, List<ContextConfigurationAttributes> configAttributes) {
return (context, mergedConfig) -> {
// This is where we use the thread local since I couldn't figure out how to get the container reference across to Spring
CustomContainer container = THREAD_CONTAINER.get();
if (container != null) {
TestPropertyValues.of("spring.datasource.url=" + container.getJdbcUrl(),
"spring.datasource.username=" + container.getUsername(),
"spring.datasource.password=" + container.getPassword())
.applyTo(context.getEnvironment());
}
};
}
}
The test class can then use @ExtendWith({DatabaseExtension.class, SpringExtension.class})
without any additional changes.
it doesn't have to be marked as reusable if you only need to use the same container between the tests. Reusable Containers feature is for reusing containers between the test runs (think running
mvn test
twice)
I am very surprised about this statement @bsideup . In the documentation it says https://www.testcontainers.org/test_framework_integration/junit_5/ that:
- an static @Container is reused across whole test class
- a dynamic @Container is not reused at all
So if reuse is for reusing across JVMs, what is the setup to reuse the container across test classes (/ JVM-wise?)