Not sure whether to create the issue in the spring-framework or here.

I have the following test case where I want to run it if a profile is activated.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers(disabledWithoutDocker = true)
@EnabledIf(expression = "#{environment.acceptsProfiles('pname')}", loadContext = true)
class TcTestApplicationTests {
    @Container
    @ServiceConnection
    static final GenericContainer<?> redisContainer = new GenericContainer<>("redis:7").withExposedPorts(6379);

    @Test
    void contextLoads() {
        assertThat(redisContainer.isRunning()).isTrue();
    }
}

If I run the test, following error is produced.

org.junit.jupiter.engine.execution.ConditionEvaluationException: Failed to evaluate condition [org.springframework.test.context.junit.jupiter.EnabledIfCondition]: Failed to load ApplicationContext for [WebMergedContextConfiguration@50fd739d testClass = com.pmverma.tctest.TcTestApplicationTests, locations = [], classes = [com.pmverma.tctest.TcTestApplication], contextInitializerClasses = [], activeProfiles = [], propertySourceDescriptors = [], propertySourceProperties = ["org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true", "server.port=0"], contextCustomizers = [org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@29182679, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@5183d589, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@72805168, org.springframework.boot.test.autoconfigure.actuate.observability.ObservabilityContextCustomizerFactory$DisableObservabilityContextCustomizer@1f, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizer@2f879bab, org.springframework.boot.testcontainers.service.connection.ServiceConnectionContextCustomizer@ae75782c, org.springframework.boot.test.context.SpringBootTestAnnotation@2625e0fa], resourceBasePath = "src/main/webapp", contextLoader = org.springframework.boot.test.context.SpringBootContextLoader, parent = null]
        at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
        at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
        at java.base/java.util.stream.StreamSpliterators$WrappingSpliterator.tryAdvance(StreamSpliterators.java:300)
        at java.base/java.util.stream.Streams$ConcatSpliterator.tryAdvance(Streams.java:727)
        at java.base/java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:129)
        at java.base/java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:527)
        at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:513)
        at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
        at java.base/java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:150)
        at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
        at java.base/java.util.stream.ReferencePipeline.findFirst(ReferencePipeline.java:647)
        at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: java.lang.IllegalStateException: Failed to load ApplicationContext for [WebMergedContextConfiguration@50fd739d testClass = com.pmverma.tctest.TcTestApplicationTests, locations = [], classes = [com.pmverma.tctest.TcTestApplication], contextInitializerClasses = [], activeProfiles = [], propertySourceDescriptors = [], propertySourceProperties = ["org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true", "server.port=0"], contextCustomizers = [org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@29182679, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@5183d589, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@72805168, org.springframework.boot.test.autoconfigure.actuate.observability.ObservabilityContextCustomizerFactory$DisableObservabilityContextCustomizer@1f, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizer@2f879bab, org.springframework.boot.testcontainers.service.connection.ServiceConnectionContextCustomizer@ae75782c, org.springframework.boot.test.context.SpringBootTestAnnotation@2625e0fa], resourceBasePath = "src/main/webapp", contextLoader = org.springframework.boot.test.context.SpringBootContextLoader, parent = null]
        at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:180)
        at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:130)
        at org.springframework.test.context.junit.jupiter.SpringExtension.getApplicationContext(SpringExtension.java:355)
        at org.springframework.test.context.junit.jupiter.AbstractExpressionEvaluatingCondition.evaluateExpression(AbstractExpressionEvaluatingCondition.java:161)
        at org.springframework.test.context.junit.jupiter.AbstractExpressionEvaluatingCondition.evaluateAnnotation(AbstractExpressionEvaluatingCondition.java:110)
        at org.springframework.test.context.junit.jupiter.EnabledIfCondition.evaluateExecutionCondition(EnabledIfCondition.java:46)
        ... 12 more
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'redisConnectionFactory' defined in class path resource [org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.class]: Failed to instantiate [org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory]: Factory method 'redisConnectionFactory' threw exception with message: Mapped port can only be obtained after the container is started
        at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:651)
        at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:639)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1334)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1164)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:561)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:521)
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325)
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323)
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:975)
        at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:959)
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624)
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:334)
        at org.springframework.boot.test.context.SpringBootContextLoader.lambda$loadContext$3(SpringBootContextLoader.java:137)
        at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:58)
        at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:46)
        at org.springframework.boot.SpringApplication.withHook(SpringApplication.java:1454)
        at org.springframework.boot.test.context.SpringBootContextLoader$ContextLoaderHook.run(SpringBootContextLoader.java:552)
        at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:137)
        at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:108)
        at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:225)
        at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:152)
        ... 17 more
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory]: Factory method 'redisConnectionFactory' threw exception with message: Mapped port can only be obtained after the container is started
        at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:177)
        at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:647)
        ... 42 more
Caused by: 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:512)
        at org.testcontainers.containers.ContainerState.getMappedPort(ContainerState.java:161)
        at java.base/java.util.Optional.map(Optional.java:260)
        at org.testcontainers.containers.ContainerState.getFirstMappedPort(ContainerState.java:143)
        at org.springframework.boot.testcontainers.service.connection.redis.RedisContainerConnectionDetailsFactory$RedisContainerConnectionDetails.getStandalone(RedisContainerConnectionDetailsFactory.java:60)
        at org.springframework.boot.autoconfigure.data.redis.RedisConnectionConfiguration.getStandaloneConfig(RedisConnectionConfiguration.java:84)
        at org.springframework.boot.autoconfigure.data.redis.LettuceConnectionConfiguration.createLettuceConnectionFactory(LettuceConnectionConfiguration.java:124)
        at org.springframework.boot.autoconfigure.data.redis.LettuceConnectionConfiguration.createConnectionFactory(LettuceConnectionConfiguration.java:114)
        at org.springframework.boot.autoconfigure.data.redis.LettuceConnectionConfiguration.redisConnectionFactory(LettuceConnectionConfiguration.java:93)
        at java.base/java.lang.reflect.Method.invoke(Method.java:580)
        at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:140)
        ... 43 more

Once I comment out @EnabledIf, the test runs without any issues.

Comment From: wilkinsona

Your expression requires the application context to be loaded as it's accessing the context's Environment. This creation of the context requires redisContainer to have been started so that its port information can be accessed and used to configure Lettuce to connect to the Redis instance in the container. I believe that this ordering is controlled by JUnit and that the class-level condition @EnabledIf will always be called before the beforeAll callback in @Testcontainers that would start the static container`.

Even if the ordering could be tweaked so that the container was started before the expression was evaluated (a method-level @EnabledIf may achieve this), you'd potentially waste a large amount of time if that evaluation returned false as the container and context would have been started unnecessarily. As an alternative, I would consider using a different method to control whether the tests should run. One option could be to use JUnit's @Tag. Another would be to use an expression that does not require the context to be loaded, such as one that refers only to environment variables and/or system properties.

/cc @sbrannen for awareness and in case there's anything that can be done on the Framework side to make @EnabledIf more flexible in situations where you're happy to accept the potential for wasting time by starting a context and a container that won't be used.

Comment From: sbrannen

Thanks for pinging me, @wilkinsona.

Your analysis is spot on.

Spring's @EnabledIf can be very useful, but it's not a silver bullet, especially in scenarios like the one here.

That's why loadContext is false by default. If anyone sets that to true, they should read the Javadoc for the loadContext attribute and accept the consequences.