Mocking Spring Boot Configurations Using Dependency Injection

Mocking Spring Boot Configurations Using Dependency Injection

Overview

In a proper web service, configurations are externalized to a property file to make the value consistent among usages and prevents the shotgun surgery anti-pattern. In Spring Boot, this is done by using the application.properties or the application.yml file.

Values from the property file can be injected to the beans by annotating the bean's property to set its value from the one from the property file, or annotating the constructor parameter just like how we do autowire-by-constructor.

For example, let us look at this YML file and how to inject its value to the beans.

my-app:
  security:
    client-id: b6219b53-a69d-416e-9e71-fa8f846206cd
    client-secret: bed8a7f2c39511459027e775d5dfb0e370dc41c9
  encryption:
    algo: AES_256
    key: c4dec5dfdd9a08c414ae

Injecting to a String Property

@Service
class PasswordServiceImpl implements PasswordService {

    @Value("${my-app.encryption.algo}")
    private String algo;

    @Value("${my-app.encryption.key}")
    private String encryptionKey;
   
    // remainder of the class...
 }
Injecting properties to algo and encryptionKey properties.

The problem with this setup, mocking this class is relatively hard. Searching for solutions online will lead you to org.springframework.test.util.ReflectionTestUtils.

class PasswordServiceImplTest {

    private PasswordService service;

    PasswordServiceImplTest() {
        service = new PasswordServiceImpl();
        
        ReflectionTestUtils.setField(service, "algo", "my-algo");
        ReflectionTestUtils.setField(service, "encryptionKey", "abcde123");
    }

    // Tests goes here...
Setting field values using reflection.

The values are easily set by using the .setField() method. The problem by using ReflectionTestUtils is the process is done using reflection. Reflection is the ability to modify its own code on runtime. In the case of .setField(), it sets the field public and assigns the value directly to the field.

Using ReflectionTestUtils looks clean, but what happens under the hood is really dirty. There's no cleanup code and might affect data of other tests; remember the "I" in the Test F.I.R.S.T. principle: Tests should be independent.

Injecting using Autowire

@Service
class PasswordServiceImpl implements PasswordService {

   private final String algo;
   private final String encryptionKey;
   
   @Autowired
   PasswordServiceImpl(
     @Value("${my-app.encryption.algo}") String algo,
     @Value("${my-app.encryption.key}") String encryptionKey
   ) {
     this.algo = algo;
     this.encryptionKey = encryptionKey;
   }

   // remainder of the class...
 }
Long argument list is already frowned upon. Making it longer with additional annotation, makes it look crowded.

This is a simpler approach, and take properly takes advantage of dependency injection. The only problem here is that the syntax is subjectively uglier than using @Value to a property rather than the constructor parameter.

public class PasswordServiceImplTest {
  
    private PasswordService service;
  
    @Before
    public void setup() {
        service = new PasswordServiceImpl("my_algo", "abde123");
    }

    // Tests goes here...
}
This looks cleaner, will it work with Mockito?

One bad thing about this format is Mockito's @InjectMocks is not possible anymore since you cannot really mock strings. In general, mocking final class is not possible like the String class. That is why we are just passing strings as the mocked dependency.

Configuration Properties

Configuration property is a bean that abstracts the property file of the application. This is a better method to inject values from the property file.

When using a configuration property bean, all property keys are contained in a single class. Unlike using @Value annotation where you need to write the property key to all its usages. Additionally, configuration property beans will fail to initialize if the key is unmappable from the property file.

Installing Dependency

Its dependency, spring-boot-configuration-processor, must be installed in the project:

annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
Gradle Version: The dependency entry for the build.gradle file.
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-configuration-processor</artifactId>
	<optional>true</optional>
</dependency>
Maven Version: The dependency entry for the pom.xml file.

Creating Configuration Property Bean

Just create a bean with @ConfigurationProperties annotation:

@Component
@ConfigurationProperties("my-app.encryption")
public class EncryptionConfig {

    private String algo;
    private String key;
    
    // Setters and getters...
}
The property map my-app.encryption mapped to a configuration property bean.

When creating a configuration property bean, can be made with these easy steps:

Step 1 – Create the configuration class

Create a concrete class. For some reason, interfaces doesn't work, and make sure the class is not final. Remember that final classes are not easily mockable. In the example above, EncryptionConfig is the class that will hold the property values.

When attempting to create a configuration property bean in Kotlin using the data class construct, a bean can be built but it cannot be mocked since Kotlin data classes are final. To work around this, make the config class implement an interface:

interface EncrpytionConfig {
    val algo: String
    val key: String
}

@ConstructorBinding
@ConfigurationProperties("my-app.encryption")
data class EncrpytionConfigImpl(
    override val algo: String,
    override val key: String
)

// Example bean declaration
private val encryptionConfig: EncryptionConfig
Making a configuration property bean work with a final class (Kotlin data class are final.) Notice the use of @ConstructorBinding – this tells Spring to bind using the constructor, not the setters.

Step 2 – Tell Spring which configuration property to bind to the class

Using the @ConfigurationProperties annotation, Spring will see the annotated class a property configuration bean. To determine which object from the property file to bind to the bean, just set the annotation's value to the key of property object to bind.

In the example, "my-app.encryption" is the key of the property object to bind.

Step 3 – Declare the property keys as the class properties

The class properties will have the same name as its keys from the property file. In the example above, algo is mapped to String algo and key is mapped to String key. When the key has multiple words, such as client-id and client-secret, just write the kebab-cased keys to camel case. Therefore in the example, client-id is written as clientId, and client-secret is written as clientSecret.

When mapping a nested object, another class should be created to map the internal object. The nested object does not need to be annotated with @ConfigurationProperties.

my-app:
  security:
    client-id: b6219b53-a69d-416e-9e71-fa8f846206cd
    client-secret: bed8a7f2c39511459027e775d5dfb0e370dc41c9
    otp:
      base-url: https://localhost:8081/otp-service/api/v1/
      access-token: d7c83a229aabfd46077f43af355e6667
The property object my-app.security has a new key, otp.
@ConfigurationProperties("my-app.security")
public class SecurityConfig {
    private String clientId;
    private String clientSecret;
    private Otp otp;
    
    static class Otp {
        private String baseUrl;
        private String accessToken;
        // getters and setters...
    }
    
    // getters and setters...
}
Configuration class for the key my-app.security. A nested object Otp is made to map the nested object from the YML file.

It is possible to map a YML array to Java list:

my-app:
  security:
    client-id: b6219b53-a69d-416e-9e71-fa8f846206cd
    client-secret: bed8a7f2c39511459027e775d5dfb0e370dc41c9
    otp:
      base-url: https://localhost:8081/otp-service/api/v1/
      access-token: d7c83a229aabfd46077f43af355e6667
      allowed-prefixes:
        - +63
        - +1
        - +65
        - +81
Example application.yml file with an array value.
@ConfigurationProperties("my-app.security")
public class SecurityConfig {
    private String clientId;
    private String clientSecret;
    private Otp otp;
    
    static class Otp {
        private String baseUrl;
        private String accessToken;
        private List<String> allowedPrefixes;

        // getters and setters...
    }
    
    // getters and setters...
}
The configuration property my-app.security.otp.allowed-prefixes mapped to a List<String> data.

It is also possible to map YML map to a Java Map.

my-app:
  encryption:
    algo: RSA
    key: c4dec5dfdd9a08c414ae
    type-key-sizes:
      - password: 4096
      - email: 2048
      - document: 2048
Example application.yml file with a map value.
@ConfigurationProperties("my-app.encryption")
public class EncryptionConfig {

    private String algo;
    private String key;
    private Map<String, String> typeKeySizes;
    
    // Setters and getters...
}
The configuration property my-app.encryption.type-key-sizes mapped to a Map<String,String> data.

Step 4 – Make the class discoverable by component scan

The easiest part. Annotate the class with @Component.

Now that the class is a bean, it is now usable just like any Spring beans

@Service
class PasswordServiceImpl implements PasswordService {

   private final EncryptionConfig encryptionConfig;
   
   @Autowired
   PasswordServiceImpl(EncryptionConfig encryptionConfig) {
     this.encryptionConfig = encryptionConfig;
   }

   // remainder of the class...
 }
See that the our example EncryptionConfig bean is injected just like how inject other dependency beans.

Mocking a Configuration Property Bean

Since the configuration has been turned into a bean, it is now possible to mock it just like any beans or components in Spring Boot.

Mockito Example

@RunWith(MockitoJUnitRunner.class)
public class PasswordServiceImplTest {
    @Mock
    private EncryptionConfig encryptionConfig;

    @InjectMocks
    private PasswordService service;

    @Test
    public void sampleTest() {
        when( encryptionConfig.getAlgo() )
            .thenReturn("my_algo");
        when( encryptionConfig.getKey() )
            .thenReturn("abcde123");

        // remaining part of the test
    }

    // other tests...
}
Configuration values can now be mocked using Mockito's when() .thenReturn() construct.

MockK Example

class PasswordServiceImplTest {
    lateinit var encryptionConfig = mockk<EncryptionConfig>()
    
    var passwordService: PasswordService
    
    @Before
    fun setup() {
        passwordService = PasswordService(encryptionConfig)
    }

    @Test
    fun `sample test`() {
        every { encryptionConfig.algo } returns "my_algo"
        every { encryptionConfig.key } returns "abcde123"

        // remaining part of the test
    }

    // other tests...
}
Configuration values can now be mocked using MockK's every call returns value construct.

Spock Example

class PasswordServiceImplSpec {

    EncryptionConfig encryptionConfig = Mock()
    
    PasswordService service
    
    void setup() {
        service = new PasswordServiceImpl(encryptionConfig)
    }

    void "sample test"() {
        encryptionConfig.getAlgo() >> 'my_algo'
        encryptionConfig.getKey() >> 'abcde123'

        // remaining part of the test
    }

    // other tests...
}
Configuration values can now be mocked using Spock's mocking feature.

Conclusion

While there are multiple ways of injecting a value from the properties file to the class that uses it, such as using the @Value annotation to the field or the constructor parameter, prefer to use a configuration property bean.

Configuration property bean enables to developer to create an abstraction layer for the property file. With this, the properties are usable just like any Spring bean. You can think of it as any repository interface, but it interfaces the property file rather than a data table.

Now that the configuration now works like any other bean, it is now mockable just like any other bean!

Show Comments