conditionalonproperty

How to Use and Test @ConditionalOnProperty in Spring Boot

In this article we will learn how to test @ConditionalOnProperty annotation in Spring Boot. This annotation is specially useful for enabling or disabling components based on a feature flag.

Let’s start!

Introduction

The @ConditionalOnProperty annotation in Spring allows us to instantiate a component based a given condition. For example we could instantiate an implementation or another depending on a configuration property.

This annotation can also be helpful to enable or disable functionalities based on a feature flag property, or enable or disable background jobs in our application based on properties.

Usage

The basic usage of this annotation is quite simple. In order to be able to understand it easily, we’re going to write a simple example in Java.

Let’s say that we have a cache layer in our application, in order to define a common contract to access this cache layer, we have a CacheService interface. This interface looks like this:

public interface CacheService {
    Entity getById(String id);
    void save(Entity entity);
}

We will have two implementation of this interface: RedisCacheService and InMemoryCacheService. To simplify things, we are going to omit the implementation of these two classes. We’ll go straight to the point where we configure their instances!

Before we do that, we need an additional component that we will get injected the corresponding instance of the cache service. For instance, we could have:

@Service
public class EntityService {
   
    @Autowired
    private final CacheService cacheService;

    public EntityService(CacheService cacheService) {
        this.cacheService = cacheService;
    }

    .... OMIT details ...
}

Now, let’s say that we have a property named app.cache.layer.type whose values could be either redis or in-memory. Depending on the value of this property we will use one implementation or the other.

How can we do that? It’s quite simple. Let’s see how!

conditionalonproperty - configuration
Photo by Ferenc Almasi on Unsplash

Configuration

There are two ways to configure this behaviour. One of them is by using a configuration bean and using the annotation at the method level. The other is by using the annotation at the class level in each implementation.

We’ll cover both of them quickly!

Method-Level Annotation

In order to achieve this, we are going to create a configuration bean which will contain the definition of both components.

@Configuration
public class MyAppConfiguration {
   
    @Bean
    @ConditionalOnProperty(prefix="app.cache.layer", name="type", "redis")
    public CacheService redisCacheService() {
        return new RedisCacheService();
    }

    @Bean
    @ConditionalOnProperty(prefix="app.cache.layer", name="type", "in-memory")
    public CacheService inMemoryCacheService() {
        return new InMemoryCacheService();
    }
}

As you can see, we define the annotation at method-level where we define a bean for each implementation. This should be enough to tell Spring to inject one implementation or the other into our EntityService bean.

Let’s see the other option now!

Class-Level Annotation

We could also achieve the same by using this property on top of our implementations at class-level, for example:

@Service
@ConditionalOnProperty(prefix="app.cache.layer", name="type", "in-memory")
public class InMemoryCacheService implements CacheService {
   ...
}

All of this looks simple, but how can we make sure that this works?

Let’s write some tests!

Testing

To be able to test this annotation, we will need to use Spring’s ApplicationContextRunner.

Let’s see how our test would look like:

public class ConditionalCacheServiceLayerTest {

        private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
        .withUserConfiguration(MyAppConfiguration.class)
        .withUserConfiguration(EntityService.class);

    @Test
    void shouldInstantiateRedisCacheService() {
        contextRunner.withPropertyValues("app.cache.layer.type=redis")
            .run(context -> {
                assertThat(context).hasSingleBean(RedisCacheService.class);
                assertThat(context).doesNotHaveBean(InMemoryCacheService.class);
            });
    }

    @Test
    void shouldInstantiateInMemoryCacheService() {
        contextRunner.withPropertyValues("app.cache.layer.type=in-memory")
            .run(context -> {
                assertThat(context).hasSingleBean(InMemoryCacheService.class);
                assertThat(context).doesNotHaveBean(RedisCacheService.class);
            });
    }
}

As you can see, we are defining a different value for the property in each test. Once the context gets started using this property, we assert that only the implementation we expect has been instantiated and loaded in the Spring context.

Please also keep in mind that sometimes loading Spring context in this way gets tricky depending on your Spring configuration. You can mock any troubling dependency in your context to simplify things, for example you could define an inner class in your test in this way:

    static class TestAppConfiguration {
        @Bean
        SomeComponent myComponent() {
            return mock(SomeComponent.class);
        }
    }

You would have to pass your configuration bean to withUserConfiguration method in ApplicationContextRunner to make it work. A mock of that component will be instantiated instead of the real object.

And that’s it, as simple as that!

If you are interested in learning Spring in depth, we highly recommend the following books:

Conclusion

In this article we have talked about how to use ConditionalOnProperty annotation to be able to enable or disable components in Spring, something very useful when configuring different types of implementations in our application.

If you are interested in reading more of our Spring articles, you can find them here.

This is all from us today! We really hope you’ve enjoyed this article and hopefully learned something new!

Looking forward to seeing you again with us very soon! Thanks for reading us!