Given the following use case:

  • A test needs a manageable, external resource.
  • That resource has information that needs to be passed on into a test configuration or is used to create additional beans.

See the following example 1:

https://github.com/michael-simons/bootiful-music/blob/75b8bd7540452b180d02957f1444cbc6ff41778d/knowledge/src/test/java/ac/simons/music/knowledge/domain/CountryRepositoryTest.java#L88-L99

A test container is started via the current test container extension and stored in a static field. This is used to create an additional respectively modified bean.

Another example is this one:

@NeedsCausalCluster
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = { BookmarkLoadTest.Initializer.class })
public class BookmarkLoadTest {

    @Neo4jUri
    private static String clusterUri;

    static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
        public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
            TestPropertyValues.of(
                "spring.data.neo4j.uri=" + clusterUri,
                "spring.data.neo4j.username=neo4j",
                "spring.data.neo4j.password=password"
            ).applyTo(configurableApplicationContext.getEnvironment());
        }
    }
}

The clusterUri is also supplied by managed resource. Here new values are applied to the test config.

I discussed with @sormuras the usage of https://github.com/sormuras/brahms, especially the Resource Manager Extension That extension works very well and could be used for injectable https://www.testcontainers.org.

So instead of having the above static test container field, I would like to see the following:

@TestConfiguration
static class Config {

    @Bean
    public org.neo4j.ogm.config.Configuration configuration(
        @Singleton(Neo4jContainer.class) Neo4jContainer neo4jContainer
    ) {
        var builder = new org.neo4j.ogm.config.Configuration.Builder();
        builder.uri(neo4jContainer.getBoltUrl());
        builder.withCustomProperty(ParameterConversionMode.CONFIG_PARAMETER_CONVERSION_MODE,
            ParameterConversionMode.CONVERT_NON_NATIVE_ONLY);
        return builder.build();
    }
}

respectively something like

@NeedsCausalCluster
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = { BookmarkLoadTest.Initializer.class })
public class BookmarkLoadTest {
    static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
               public Initializer(@Singleton(Neo4jContainer.class) Neo4jContainer neo4jContainer) {}

        public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
            TestPropertyValues.of(
                "spring.data.neo4j.uri=" + clusterUri,
                "spring.data.neo4j.username=neo4j",
                "spring.data.neo4j.password=password"
            ).applyTo(configurableApplicationContext.getEnvironment());
        }
    }
}

I have seen the need to pass in a configuration value based on some random ports etc. so often now and I'm convinced it would be awesome to have Spring's ParameterResolver extension be able to recognize other JUnit 5 parameter resolver as providers of "things" (as things that look like beans but aren't).

Comment From: sbrannen

Thanks for raising the issue.

The need you have expressed has in fact been expressed by many people over the years in various forms. At its core, it's a chicken-and-egg problem: "what gets created first: an external resource or the Spring ApplicationContext?".

As for the proposals:

  1. It is not possible for anything other than an existing bean from the ApplicationContext to be supplied as an argument to a @Bean method. There is no such thing as an argument resolver for @Bean methods. Consequently, a JUnit Jupiter ParameterResolver cannot be applied here.
  2. Implementations of ApplicationContextInitializer must provide a default constructor for instantiation by the framework. Although AbstractContextLoader (in spring-test) is responsible for the instantiation of such initializers within the TestContext framework, it is not possible for Spring to make use of JUnit Jupiter's parameter resolution for constructor arguments (i.e., org.junit.jupiter.engine.execution.ExecutableInvoker). For starters, ExecutableInvoker is an internal implementation detail of the JUnit Jupiter TestEngine. Secondly, a Spring SmartContextLoader has no access to testing framework specifics: the core of the Spring TestContext Framework is intentionally testing framework agnostic and will need to stay that way.

Having said that, there may be something we can do in spring-test to better support such chicken-and-egg uses cases, but that will require further brainstorming.

So, I'll leave this issue open for that purpose.

I have also updated this issue's title to reflect that.

Comment From: michael-simons

Yes, the issue keeps reappearing: Having a test resource that has unique and random information about ports and URLs during the test that are needed in the context.

Maybe Spring Framework is the wrong place after all… With @MockedBean from Boot I get external "things" into the context. Seems similar to me.

Comment From: sbrannen

With @MockedBean from Boot I get external "things" into the context. Seems similar to me.

Spring Boot Test's @MockBean support is actually based on building blocks available in spring-test, namely the ContextCustomizer infrastructure and Automatic Discovery of Default TestExecutionListener Implementations.

In other words, that kind of power is not limited to Spring Boot.

Comment From: bsideup

FYI https://github.com/spring-projects/spring-boot/issues/16886

Comment From: michael-simons

24540 with @DynamicPropertySource solves this. Excellent work.