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.
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).
- 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);
- 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*/);
}
- 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.
Great example, thanks! But what about
@ConditionalOnProperty
on interface? How it can be tested?