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...
}
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...
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...
}
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...
}
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'
build.gradle
file.<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
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...
}
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
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
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...
}
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
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...
}
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
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...
}
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...
}
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...
}
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...
}
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...
}
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!