spring-boot icon indicating copy to clipboard operation
spring-boot copied to clipboard

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

Open eduanb opened this issue 3 years ago • 4 comments

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.

eduanb avatar Mar 10 '22 08:03 eduanb

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.

wilkinsona avatar Mar 10 '22 08:03 wilkinsona

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?

snicoll avatar Mar 19 '22 08:03 snicoll

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 avatar Jun 06 '22 10:06 wilkinsona

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.

wilkinsona avatar Jun 08 '22 16:06 wilkinsona