Hey team, we have met an issue when we use spring-boot-testcontainers 3.3.0.
The Problem:
2024-05-29T13:11:55.056+10:00 WARN 25652 --- [ main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: java.lang.IllegalStateException: Mapped port can only be obtained after the container is started
How to reproduce it:
demo-project.zip
Please us the demo-project and directly run RunApplicationLocally.kt
What we have found:
If we use spring-boot-testcontainers 3.2.0, all good.
2024-05-29T13:30:55.245+10:00 INFO 26680 --- [ main] tc.localstack/localstack:3.4.0 : Creating container for image: localstack/localstack:3.4.0 2024-05-29T13:30:55.291+10:00 INFO 26680 --- [ main] o.t.utility.RegistryAuthLocator : Credential helper/store (docker-credential-osxkeychain) does not have credentials for https://index.docker.io/v1/ 2024-05-29T13:30:55.805+10:00 INFO 26680 --- [ main] tc.localstack/localstack:3.4.0 : Container localstack/localstack:3.4.0 is starting: ff868fd07f14190e93a3d4c31eedfe20c3806035dc2bbcf430a523dd7220f807 2024-05-29T13:31:01.686+10:00 INFO 26680 --- [ main] tc.localstack/localstack:3.4.0 : Container localstack/localstack:3.4.0 started in PT6.441141S Add properties with dynamic values to the Environment for integration tests! http://127.0.0.1:32831 us-east-1
If we use spring-boot-testcontainers 3.2.1 or later versions, it fails.
Add properties with dynamic values to the Environment for integration tests! 2024-05-29T13:32:49.997+10:00 WARN 26764 --- [ main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: java.lang.IllegalStateException: Mapped port can only be obtained after the container is started 2024-05-29T13:32:50.000+10:00 INFO 26764 --- [ main] .s.b.a.l.ConditionEvaluationReportLogger :
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled. 2024-05-29T13:32:50.012+10:00 ERROR 26764 --- [ main] o.s.boot.SpringApplication : Application run failed
java.lang.IllegalStateException: Mapped port can only be obtained after the container is started
Could you help us fix it up or any other way we could work it around?
Comment From: quaff
You can try annotating @org.testcontainers.junit.jupiter.Testcontainers on class WithContainers.
But it will trigger another problem, the @DynamicPropertySource method is invoked twice from DynamicPropertiesContextCustomizer and DynamicPropertySourceMethodsImporter.
Thread [main] (Suspended (breakpoint at line 22 in TestContainerTests))
owns: DefaultContextCache (id=82)
TestContainerTests.populateDynamicPropertyRegistry(DynamicPropertyRegistry) line: 22
NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method]
NativeMethodAccessorImpl.invoke(Object, Object[]) line: 77
DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 43
Method.invoke(Object, Object...) line: 568
ReflectionUtils.invokeMethod(Method, Object, Object...) line: 281
DynamicPropertiesContextCustomizer.lambda$buildDynamicPropertiesMap$3(DynamicPropertyRegistry, Method) line: 82
Thread [main] (Suspended (breakpoint at line 22 in TestContainerTests))
owns: DefaultContextCache (id=82)
TestContainerTests.populateDynamicPropertyRegistry(DynamicPropertyRegistry) line: 22
NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method]
NativeMethodAccessorImpl.invoke(Object, Object[]) line: 77
DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 43
Method.invoke(Object, Object...) line: 568
ReflectionUtils.invokeMethod(Method, Object, Object...) line: 281
DynamicPropertySourceMethodsImporter.lambda$registerDynamicPropertySources$0(DynamicPropertyRegistry, Method) line: 57
0x000000012f319850.accept(Object) line: not available
Here is the test case:
package com.example.demo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.testcontainers.context.ImportTestcontainers;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SpringJUnitConfig
//@Testcontainers
public class TestContainerTests {
@Container
static PostgreSQLContainer<?> container = new PostgreSQLContainer<>("postgres");
@DynamicPropertySource
static void populateDynamicPropertyRegistry(DynamicPropertyRegistry registry) {
System.out.println(container.getMappedPort(5432));
}
@Test
void test() {
}
@Configuration
@ImportTestcontainers(TestContainerTests.class)
static class Config {
}
}
Comment From: wilkinsona
The problem is that you're accessing the container directly within your @DynamicPropertySource method:
println(localStackContainer.getEndpointOverride(SQS))
This requires the container to have been started when the @DynamicPropertySource method is called, undoing the benefit of the registry's lazy nature where you register a property with a supplier for its value. Using a supplier means that the container does not have to have been started until the property is accessed which happens later in the application lifecycle.
This worked in 3.2.0 as the containers were started very early but that was at the expense of support for parallel startup being broken. This was fixed in #38831.
In summary, this only worked in 3.2.0 accidentally and, unfortunately, you'll have to make some changes to adapt to the way things now work. You can either remove println calls that access the container or move them into the supplier implementations where the values for the properties are being returned.