How to inject a specific subclass based on a property value using #ConfigurationProperties - spring

Having an application.yml like the following:
tests:
happy-path:
- type: single
other-property: "other-value"
- type: parallel
other-property: "another-value"
And having a Class like this one, where I have a Lists of Test type:
#ConfigurationProperties(prefix = "test")
#EnableConfigurationProperties
#ConstructorBinding
public class TestRunner {
private List<Test> happyPath;
public TestRunner(List<Test> tests) {
this.tests = tests;
}
}
Test type has two different subclass implementations, SingleTest and ParallelTest, booth extending Test class.
Anyone knows how can I tell Spring to inject an instance of SingleTest when tests.happy-path[*].type==single and inject an instance of ParallelTest when tests.happy-path[*].type==parallel so I end up with a List of Test that in reality is a List of SingleTest and ParallelTest implementations?
I've tried with Converters with no luck for now :(

Related

Is it possible to only load specific Annotations based on a profile?

Is it possible to only load specific Annotations only during tests or only during a run in Spring Boot?
I am facing a situation where there are Annotations affecting the tests, yet work well in the live run, so wanted to know whether it was possible to exclude them only during tests, but include them when running, similar to how one can include specific beans based on a Spring profile
Apologies if this has been asked before, I have tried searching to no avail
You could use the #ConditionalOnProperty annotation which creates a bean depending on which property (in the application.properties -> app.val = false) is set. For example for a service:
#Service
#ConditionalOnProperty(name = "app.val", havingValue = "false")
public class TestService {
...
}
Also you could use the #Profile annotation and annotate them to the methods which have for example a test profile (defined in the application.properties as well -> spring.profiles = test).
#Profile({"test"})
public String getValue() {
return "test value";
}
#Profile({"production"})
public String getValue() {
return "production value";
}

How to test #ConfigurationProperties with ApplicationContextRunner from spring-boot-test?

I need to test my autoconfiguration classes that make use of #ConfigurationProperties beans. I'm making use of ApplicationContextRunner as documented in https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-test-autoconfig to make tests faster and avoid starting the servlet container between each variations. However, beans annotated with #AutoconfigurationProperties are not populated with values injected into ApplicationContextRunner.
I suspect that I'm hitting problem similar to https://stackoverflow.com/a/56023100/1484823
#ConfigurationProperties are not managed by the application context you build in tests, although they will be load when the application launches, because you have #EnableConfigurationProperties on your app main class.
How can I enable support for #ConfigurationProperties with ApplicationContextRunner ?
Here is the corresponding code
#Test
void ServiceDefinitionMapperPropertiesAreProperlyLoaded() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
SingleServiceDefinitionAnswerAutoConfig.class,
DynamicCatalogServiceAutoConfiguration.class
))
// .withPropertyValues(DynamicCatalogProperties.OPT_IN_PROPERTY + "=true") //Not sure why this seems ignored
.withSystemProperties(DynamicCatalogConstants.OPT_IN_PROPERTY + "=true",
ServiceDefinitionMapperProperties.PROPERTY_PREFIX
+ServiceDefinitionMapperProperties.SUFFIX_PROPERTY_KEY+ "=suffix")
;
contextRunner.run(context -> {
assertThat(context).hasSingleBean(ServiceDefinitionMapperProperties.class);
ServiceDefinitionMapperProperties serviceDefinitionMapperProperties
= context.getBean(ServiceDefinitionMapperProperties.class);
assertThat(serviceDefinitionMapperProperties.getSuffix()).isEqualTo("suffix");
});
}
which fails with:
org.opentest4j.AssertionFailedError:
Expecting:
<"">
to be equal to:
<"suffix">
but was not.
Expected :suffix
Actual :
<Click to see difference>
at org.springframework.cloud.appbroker.autoconfigure.DynamicCatalogServiceAutoConfigurationTest
public class DynamicCatalogServiceAutoConfiguration {
[...]
#Bean
#ConfigurationProperties(prefix=ServiceDefinitionMapperProperties.PROPERTY_PREFIX, ignoreUnknownFields = false)
public ServiceDefinitionMapperProperties serviceDefinitionMapperProperties() {
return new ServiceDefinitionMapperProperties();
}
[...]
}
Full sources available at https://github.com/orange-cloudfoundry/osb-cmdb-spike/blob/0da641e5f2f811f48b0676a25c8cbe97895168d1/spring-cloud-app-broker-autoconfigure/src/test/java/org/springframework/cloud/appbroker/autoconfigure/DynamicCatalogServiceAutoConfigurationTest.java#L89-L107
ps: I was about to submit an issue to https://github.com/spring-projects/spring-boot/issues to suggest documentation enhancement to warn of such limitation in ApplicationContext, and to ask for ways to turn on support for #ConfigurationProperties. Following guidance at https://raw.githubusercontent.com/spring-projects/spring-boot/master/.github/ISSUE_TEMPLATE.md, I'm first making sure here I'm not misunderstanding the problem.
If you want to populate a bean annotated with #ConfigurationProperties class as part of your test, and you normally depend on a configuration class annotated with #EnableConfigurationProperties to populate that bean, then you can create a trivial configuration class just for the test:
#ConfigurationProperties("app")
public class ConfigProps {
private int meaningOfLife;
public int getMeaningOfLife() { return meaningOfLife; }
public void setMeaningOfLife(int meaning) { this.meaningOfLife = meaning; }
}
class ConfigPropsTest {
private final ApplicationContextRunner runner = new ApplicationContextRunner();
#EnableConfigurationProperties(ConfigProps.class)
static class TrivialConfiguration {
}
#Test
void test() {
runner.withUserConfiguration(TrivialConfiguration.class)
.withPropertyValues("app.meaning-of-life=42")
.run(context -> {
assertEquals(42, context.getBean(ConfigProps.class).getMeaningOfLife());
});
}
}
Passing TrivialConfiguration to the ApplicationContextRunner is sufficient to make it create ConfigProps and populate it using the available properties.
As far as I can tell, none of the classes involved in your test enable configuration property binding. As a result, no properties are bound to ServiceDefinitionMapperProperties. You can enable configuration property binding using #EnableConfigurationProperties. A typical place to add it would be on DynamicCatalogServiceAutoConfiguration as its serviceDefinitionMapperProperties bean relies on configuration properties being enabled.

#Value annotation is only loading default - not using property file

Problem
I think that I havn't understood something properly because my #Value is always loading the default calue.
Java Code
So I have the following:
#Value("${disableQuerySecurityDebug:false}")
private boolean disableQuerySecurityDebug;
And this is set to false always.
Property file: application-disableQuerySecurityDebug.properties
I have a properties file called application-disableQuerySecurityDebug.properties.
And I have the following entry inside the file:
disableQuerySecurityDebugMne=true
And I run the application with the following profile: disableQuerySecurityDebugMne
I was expecting the value to be set to true, but it is always false.
Update
Based on deadpool's answer, I ended up with the following:
#Profile("disableQuerySecurityDebug") #Data
#Configuration
public class DisableSecurityConfig implements DisableQuerySecurityDebug {
#Value("${disableQuerySecurityDebug:true}")
private boolean securityDisabled;
}
#Profile("!disableQuerySecurityDebug") #Data
#Configuration
public class EnableSecurityConfig implements DisableQuerySecurityDebug{
#Value("${disableQuerySecurityDebug:false}")
private boolean securityDisabled;
}
public interface DisableQuerySecurityDebug{
public boolean isSecurityDisabled();
}
#Value annotation is only used to inject properties values into spring Beans from yml or properties file
This annotation can be used for injecting values into fields in Spring-managed beans and it can be applied at the field or constructor/method parameter level.
If you want to inject values based on profile specific then use #Profile on class
#Profile("disableQuerySecurityDebug")
#Configuration
public class Config {
#Value("${disableQuerySecurityDebug:false}")
private boolean disableQuerySecurityDebug;
}
You could also specify it on the command line by using the following switch:
java -jar demo.jar --spring.profiles.active=disableQuerySecurityDebug

Spring How to map list of custom pojo

In my application.yml file, I want to define a list of rules.
rules:
- name: abc
value: something
- name: edf
value: something
Then I want to define a service like this
#Service
public class MyService {
public MyService(#Value("${rules}") List<Rule> rules) {
}
}
For the Rule pojo, it's like this.
public class Rule {
public String name, value;
}
Currently, I'm facing these errors.
If I leave rules empty, it throws can't convert String to List<Rule>
rules: []
If I keep the values, it throws could not resolve placeholder 'rules'
I really don't know what I'm doing wrong here.
From Spring docs, I found this.
Using the #Value("${property}") annotation to inject configuration
properties can sometimes be cumbersome, especially if you are working
with multiple properties or your data is hierarchical in nature.
Spring Boot provides an alternative method of working with properties
that lets strongly typed beans govern and validate the configuration
of your application
Link: https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-typesafe-configuration-properties
At the end, I have to introduce another class.
#Configuration
#ConfigurationProperties(prefix="rules")
public class Rules {
public List<Rule> list;
}
Then I autowire it in MyService.

How can I inject config properties into a unit test, using SpringBoot2, JUnit5, and Kotlin

My scenario:
I'm building an app that uses Kotlin and SpringBoot 2.0.3. I'm trying to write all my unit tests in JUnit5. All 3 of these are new to me, so I'm struggling a bit.
I'm using a #ConfigurationProperties class (instead of #Value) to inject values from my application.yml into my Spring context.
#Configuration
#ConfigurationProperties(prefix = "amazon.aws.s3")
class AmazonS3Config {
val s3Enabled: Boolean = false
val region: String = ""
val accessKeyId: String = ""
val secretAccessKey: String = ""
val bucketName: String = ""
}
I then have a Kotlin class that is utilizing these properties, following Kotlin/Spring best practice to define the injected class as a constructor parameter.
class VqsS3FileReader(val amazonS3Config: AmazonS3Config) : VqsFileReader {
companion object: mu.KLogging()
override fun getInputStream(filePath: String): InputStream {
val region: String = amazonS3Config.region
val accessKeyId: String = amazonS3Config.accessKeyId
val secretAccessKey: String = amazonS3Config.secretAccessKey
val bucketName: String = amazonS3Config.bucketName
logger.debug { "The configured s3Enabled is: $s3Enabled" }
logger.debug { "The configured region is: $region" }
logger.debug { "The configured accessKeyId is: $accessKeyId" }
logger.debug { "The configured secretAccessKey is: $secretAccessKey" }
logger.debug { "The configured bucketName is: $bucketName" }
val file: File? = File(filePath)
//This method is not yet implemented, just read a file from local disk for now
return file?.inputStream() ?: throw FileNotFoundException("File at $filePath is null")
}
}
I have not completed this implementation, as I'm trying to get the unit test working first. So for the moment, this method doesn't actually reach out to S3, just streams a local file.
My unit test is where I'm getting stuck. I don't know how to inject the properties from my application.yml into the test context. Since the ConfigProperty class is passed as a construction parameter, I have to pass it when I establish my service in my unit test. I've tried various solutions that don't work. I found this piece of info, which was helpful:
If Spring Boot is being used, then #ConfigurationProperties instead of #Value annotations can be used, but currently this only works with lateinit or nullable var properties (the former is recommended) since immutable classes initialized by constructors are not yet supported.
So this means I cannot use class VqsS3FileReaderTest(amazonS3Config: AmazonS3Config): TestBase() { ... } and then pass the config to my service.
This is what I have currently:
#ActiveProfiles("test")
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
#ExtendWith(SpringExtension::class)
#ContextConfiguration(classes = [AmazonS3Config::class, VqsS3FileReader::class])
class VqsS3FileReaderTest(): TestBase() {
#Autowired
private lateinit var amazonS3Config: AmazonS3Config
#Autowired
private lateinit var fileReader: VqsS3FileReader
val filePath: String = "/fileio/sampleLocalFile.txt"
#Test
fun `can get input stream from a valid file path` () {
fileReader = VqsS3FileReader(amazonS3Config)
val sampleLocalFile: File? = getFile(filePath) //getFile is defined in the TestBase class, it just gets a file in my "resources" dir
if (sampleLocalFile != null) {
val inStream: InputStream = fileReader.getInputStream(sampleLocalFile.absolutePath)
val content: String = inStream.readBytes().toString(Charset.defaultCharset())
assert.that(content, startsWith("Lorem Ipsum"))
} else {
fail { "The file at $filePath was not found." }
}
}
}
With this, my test runs, and my context seems to setup properly, but the properties from my application.yml are not being injected. For my debug output, I see the following:
08:46:43.111 [main] DEBUG com.ilmn.vqs.fileio.VqsS3FileReader - The configured s3Enabled is: false
08:46:43.111 [main] DEBUG com.ilmn.vqs.fileio.VqsS3FileReader - The configured region is:
08:46:43.112 [main] DEBUG com.ilmn.vqs.fileio.VqsS3FileReader - The configured accessKeyId is:
08:46:43.112 [main] DEBUG com.ilmn.vqs.fileio.VqsS3FileReader - The configured secretAccessKey is:
08:46:43.112 [main] DEBUG com.ilmn.vqs.fileio.VqsS3FileReader - The configured bucketName is:
All empty strings, which is the default values. Not the values I have in my application.yml:
amazon.aws.s3:
s3Enabled: true
region: us-west-2
accessKeyId: unknown-at-this-time
secretAccessKey: unknown-at-this-time
bucketName: test-bucket
I see mistake in the following line:
#ContextConfiguration(classes = [AmazonS3Config::class, VqsS3FileReader::class])
Please put configuration classes here (instead of just beans).
Short - hot to fix test
Create class (if missing) like VqsS3Configration in the main module (e.g. in the module, where you have production code)
Create class like VqsS3TestConfigration in the same package with your tests. Content on this file:
#org.springframework.context.annotation.Configuration // mark, that this is configuration class
#org.springframework.context.annotation.Import(VqsS3Configration::class) // it references production configuration from test configuration
#org.springframework.context.annotation.ComponentScan // ask Spring to autoload all files from the package with VqsS3TestConfigration and all child packages
class VqsS3TestConfigration {
/*put test-related beans here in future*/
}
Then go to test and change declaration:
#ContextConfiguration(classes = [VqsS3TestConfigration ::class]) // we ask Spring to load configuration here
I created sample application here: https://github.com/imanushin/spring-boot2-junit5-and-kotlin-integration
Please execude line .\gradlew.bat test or gradlew.bat bootRun in the src folder. Test will check, that we able to read properties. bootRun will print auto-loaded properties
Boring theory
First of all - Spring has Configuration classes - they are needed to load and initialize other classes. Instead of Service or Comonent classes, main purpose of Configuration classes - just create services, components, etc.
If we will simplify algorithm of the Spring application load, then it will be like this:
Find Configuration classes
Read annotation of them, understand list of classes (e.g. reference tree), which should be loaded (and in addition - how they should be loaded)
Load classes with different ways:
3.1. For classes, which are annotated with #ConfigurationProperties - put configuration items here
3.2. For classes, which are annotated with #RestController - register them as rest controllers
3.N. etc...
How does Spring understand, what configuration should be loaded?
Formally is it done by Spring Boot, however I will name it as Spring
Understand several initial configurations - they can be put into the class SpringApplicationBuilder, into the test annotations (see above), into the XML context, etc. For our case we use test annotation and #ContextConfiguration attribute
Recursive get all imported configuration (e.g. Spring reads #Import annotation, then it get children, then it check their imports, etc.)
Use Spring Factories to get configuration automatically from jar
Therefore, in our case, Spring will do actions like this:
Get configuration from test annotation
Get all other configurations by recursive way
Load all classes into the contet
Start test
Okay, it took me all day, but I finally got my application properties to load into my unit test context. I made 2 changes:
First, I added the #Service annotation to my VqsS3FileReader service - which I had originally forgotten. Also, while I had updated my Test to not inject the AmazonS3Config via the constructor, I had neglected to update my service to do the same. So I changed
this:
class VqsS3FileReader(val amazonS3Config: AmazonS3Config) : VqsFileReader {
companion object: mu.KLogging()
...
to this:
#Service
class VqsS3FileReader : VqsFileReader {
companion object: mu.KLogging()
#Resource
private lateinit var amazonS3Config: AmazonS3Config
...
Finally, I modified my Spring annotations on my test.
from this:
#ActiveProfiles("test")
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
#ExtendWith(SpringExtension::class)
#ContextConfiguration(classes = [AmazonS3Config::class, VqsS3FileReader::class])
class VqsS3FileReaderTest(): TestBase() {
...
to this:
#ActiveProfiles("test")
#SpringBootTest
#ComponentScan("com.ilmn.*")
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
#ExtendWith(SpringExtension::class)
#EnableAutoConfiguration
#SpringJUnitConfig(SpringBootContextLoader::class)
class VqsS3FileReaderTest(): TestBase() {
...
It seems like I have an unordinary amount of annotations on my test now... so I will be looking carefully at what each of them really do, and see if I can reduce it. But at least my properties are being injected into my test context now.

Resources