spring-projects / spring-boot

Spring Boot
https://spring.io/projects/spring-boot
Apache License 2.0
73.53k stars 40.31k forks source link

Provide a more concise way to customize the system environment when using ApplicationContextRunner #30153

Open eduanb opened 2 years ago

eduanb commented 2 years ago

For some specific cases, it is necessary to set systemEnvironment which is different from systemProperties. For example, mocking out a Kubernetes environment. Adding a test for Kubernetes in ConditionalOnCloudPlatformTests is currently not possible.

wilkinsona commented 2 years ago

Thanks for the suggestion.

Adding a test for Kubernetes in ConditionalOnCloudPlatformTests is currently not possible

It is possible, although it's quite cumbersome. It can be achieved by using a custom supplier for the runner's context and replacing the system environment property source:

@Test
void outcomeWhenKubernetesPlatformPresentShouldMatch() {
    new ApplicationContextRunner(() -> {
        ConfigurableApplicationContext context = new AnnotationConfigApplicationContext();
        Map<String, Object> systemEnvironment = new HashMap<>();
        systemEnvironment.put("KUBERNETES_SERVICE_HOST", "k8s.example.com");
        systemEnvironment.put("KUBERNETES_SERVICE_PORT", "4567");
        context.getEnvironment().getPropertySources()
                .replace(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, new MapPropertySource(
                        StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, systemEnvironment));
        return context;
    }).withUserConfiguration(KubernetesPlatformConfig.class).run((context) -> assertThat(context).hasBean("foo"));
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES)
static class KubernetesPlatformConfig {

    @Bean
    String foo() {
        return "foo";
    }

}

We can consider removing some of the boilerplate here by providing methods on the runner specifically for manipulating the system environment. I'm not sure how common this use-case is, so let's see if the rest of the team thinks making it easier is worthwhile.

snicoll commented 2 years ago

The use case does not seem very common so I wonder if it warrants a dedicated method on the runner. I am also sightly worried that systemEnvironment and systemProperties can confuse users. The runner has higher-level way to customize things before the context run (Function). Perhaps we could offer an implementation on the side that users can apply for this use case?

wilkinsona commented 2 years ago

Thanks, @snicoll. I'm not sure that the Function would help here as there's no good way to plug this into an existing ApplicationContextRunner. withInitializer does not work for the example above as @ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) is evaluated before the initializer is called. Confusingly, this does work:

@Test
void outcomeWhenKubernetesPlatformPresentShouldMatch() {
    new ApplicationContextRunner().withInitializer((context) -> {
        Map<String, Object> systemEnvironment = new HashMap<>();
        systemEnvironment.put("KUBERNETES_SERVICE_HOST", "k8s.example.com");
        systemEnvironment.put("KUBERNETES_SERVICE_PORT", "4567");
        context.getEnvironment().getPropertySources()
                .replace(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, new MapPropertySource(
                        StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, systemEnvironment));
    }).withUserConfiguration(KubernetesPlatformConfig.class).run((context) -> assertThat(context).hasBean("foo"));
}

@Configuration(proxyBeanMethods = false)
static class KubernetesPlatformConfig {

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES)
    static class SomeInnerConfiguration {

        @Bean
        String foo() {
            return "foo";
        }

    }

}

Nesting the use of @ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) defers its evaluation until after the initializer has been called.

Flagging for a team meeting as I'd like to discuss whether we should revise the ordering in the runner:

https://github.com/spring-projects/spring-boot/blob/c996e4335af807818205e9b3b3300598daf038d2/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunner.java#L405-L410

The current arrangement means that conditions on the registered classes themselves are evaluated before the initializers and bean registrations are applied.

wilkinsona commented 2 years ago

I've opened https://github.com/spring-projects/spring-boot/issues/31280 to re-order things in ApplicationContextRunner. We'll leave this issue open for now while we decide if we want to do anything to ease configuring the system environment.