Skip to content

Instantly share code, notes, and snippets.

@Toparvion
Last active March 15, 2024 22:07
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save Toparvion/26e2baf3b3fb89fa2f957e78fde5ef53 to your computer and use it in GitHub Desktop.
Save Toparvion/26e2baf3b3fb89fa2f957e78fde5ef53 to your computer and use it in GitHub Desktop.
JUnit test for conditional Spring Boot bean registration (@ConditionalOnProperty)

Problem

Suppose you have two classes that should be registered with Spring context exclusively, e.g. only one of the beans must exist in the context at any time based on some boolean property value. Of course you can do it by adding explicit if condition into your @Configuration class. But if the classes have no common interface it may be quite cumbersome. As an alternative you can use @ConditionalOnProperty annotation on your classes, e.g.:

@Service
@ConditionalOnProperty(name = "use-left-service", havingValue = "true", matchIfMissing = false)
public class LeftService

and its partner

@Service
@ConditionalOnProperty(name = "use-right-service", havingValue = "false", matchIfMissing = true)
public class RightService

This code registers LeftService bean if use-left-service property is true and RightService if the property is false, defaulting to RightService in case of the property absence.

The behavior of @ConditionalOnProperty is not the most straightforward so you should ensure you use it correctly. For instance, if you forget to add matchIfMissing attribute and there would be no explicit property in the environment then you may end up with no bean registered at all. To avoid this, beans registration logic must be covered with unit tests. Spring Boot has a suitable test harness for it, see 47.4 Testing your Auto-configuration.

Solution

The idea behind such a test is that you create an ApplicationContextRunner instance, fill it with classes involved in bean registration process and then call its run method with appropriate assertions (usually expressed with AssertJ).

  1. Create ApplicationContextRunner instance and fill it with bean candidates.
    It's better to declare the runner as reusable component of your test class, e.g. as a field:
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
            .withUserConfiguration(LeftService.class, RightService.class);
  1. Supplement the runner with changing parts of the environment in every unit test and run it:
  @Test
  void rightServiceIsAppliedProperly() {
    contextRunner
        .withPropertyValues("use-left-service=false")  // here is the changing part (property value)
        .run(/*will see later*/);
  }
  1. Add assertions on the context and/or beans in run's method lambda:
        .run(context -> assertAll(
            () -> assertThat(context).hasSingleBean(RightService.class),
            () -> assertThat(context).doesNotHaveBean(LeftService.class)));

Note that assertAll method is imported from org.junit.jupiter.api.Assertions class while assertThat is imported from equally named org.assertj.core.api.Assertions class. Wrapping into assertAll method is used here just to perform all the nested assertions even the first one fails.

With this test you don't have to build up Spring application context with @SpringBootTest annotation and SpringRunner. The test stays almost trivial but checks for some real infrastructual behavior applying to your beans.

You can also add some extra (mock) dependencies into your test context by means of configuration classes, see the following SelectServiceBeanTest.java class.

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
class SelectServiceBeanTest {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withInitializer(new ConditionEvaluationReportLoggingListener()) // to print out conditional config report to log
.withUserConfiguration(TestConfig.class) // satisfy transitive dependencies of beans being tested
.withUserConfiguration(LeftService.class, RightService.class);
@Test
void rightServiceIsAppliedProperly() {
contextRunner
.withPropertyValues("use-left-service=false")
.run(context -> assertAll(
() -> assertThat(context).hasSingleBean(RightService.class),
() -> assertThat(context).doesNotHaveBean(LeftService.class)));
}
@Test
void leftServiceIsAppliedProperly() {
contextRunner
.withPropertyValues("use-left-service=true")
.run(context -> assertAll(
() -> assertThat(context).hasSingleBean(LeftService.class),
() -> assertThat(context).doesNotHaveBean(RightService.class)));
}
@Test
void rightServiceIsAppliedByDefault() {
contextRunner
.run(context -> assertAll(
() -> assertThat(context).hasSingleBean(RightService.class),
() -> assertThat(context).doesNotHaveBean(LeftService.class)));
}
@Configuration // this annotation is not required here as the class is explicitly mentioned in `withUserConfiguration` method
protected static class TestConfig {
@Bean
public SomeThirdDependency someThirdDependency() {
return Mockito.mock(SomeThirdDependency.class); // this bean will be automatically autowired into tested beans
}
}
}
@uladzislau-slk
Copy link

Great example, thanks! But what about @ConditionalOnProperty on interface? How it can be tested?

@Toparvion
Copy link
Author

@vsiliuk , since @ConditionalOnProperty annotation controls the registration of beans, it is commonly used with bean classes, not interfaces. Another use case is to compose a "meta" annotation like:

// ...
@ConditionalOnProperty(name = "choices.custom.auto-reload-enabled", havingValue = "true", matchIfMissing = false)
@interface ConditionalOnChoicesAutoReloadEnabled { }

, but here the annotated element is not the "ordinary" interface as well.

Can you please clarify your question with an example?

@maggalka
Copy link

maggalka commented Jul 27, 2020

@Toparvion thank you very much, this works!

I think a question of @vsiliuk was about a case when we are trying to use interface in
.withUserConfiguration(TestConfig.class)

I used to implement an interface in this case like this:

private static class TestConfigImpl implements TestConfig {
// overrided methods here
 }

@Configuration
private static class AdditionalConfig {

        @Bean
        public TestConfigImpl testConfig() {
            return Mockito.mock(TestConfigImpl.class);
        }
    }

@Toparvion
Copy link
Author

@maggalka, thanks for the clarification and your own example! 👍

@stoan
Copy link

stoan commented Feb 22, 2021

Thank you @Toparvion Great example, just what I was looking for.

Copy link

ghost commented Mar 19, 2021

Beautiful examples. Thank you!

@TJReinert
Copy link

Awesome example!

@SAP0312
Copy link

SAP0312 commented Mar 2, 2022

In case we're writing integration test cases for both LeftService and RightService then how can we make sure that only respective test cases are running based on the application property use-left-service or use-right-service?

@JithendraSR
Copy link

Thanks so much for the beautiful explanation @Toparvion.

@arobld00
Copy link

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment