In our test codebase, we are trying to minimize the usage of @SpringBootTest annotations in favor of more targeted @SpringJUnitConfig.
Replacing it in the first of the working tests:
@SpringJUnitConfig
//@SpringBootTest
class DataMarshallerServiceTest {
@Autowired
private DataMarshallerService cut;
we start seeing the following stack:
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.abc.globalpayments.feeds.downstream.dailycashreport.generate.DataMarshallerService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1801)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1357)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1311)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:659)
... 75 common frames omitted
Similarly, during the build:
mvn clean package -P mvnProfile
[ERROR] Errors:
[ERROR] DataMarshallerServiceTest.testSuccessfulFileGenerationViaBatches � UnsatisfiedDependency Error creating bean with name 'com.abc.globalpayments.feeds.downstre
am.dailycashreport.generate.DataMarshallerServiceTest': Unsatisfied dependency expressed through field 'cut'; nested exception is org.springframework.beans.factory.NoS
uchBeanDefinitionException: No qualifying bean of type 'com.abc.globalpayments.feeds.downstream.dailycashreport.generate.DataMarshallerService' available: expected at
least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
[ERROR] DataMarshallerServiceTest.testTrailerAggregationsProduction � UnsatisfiedDependency Error creating bean with name 'com.abc.globalpayments.feeds.downstream.da
ilycashreport.generate.DataMarshallerServiceTest': Unsatisfied dependency expressed through field 'cut'; nested exception is org.springframework.beans.factory.NoSuchBe
anDefinitionException: No qualifying bean of type 'com.abc.globalpayments.feeds.downstream.dailycashreport.generate.DataMarshallerService' available: expected at least
1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
[INFO]
Participating in these tests, there are @Beans created as part of @Configuration classes as well as the ones adorned with @Component and @Service to be resolved via classpath scanning.
For example, the above class under test:
@Slf4j
@Service
public class DataMarshallerService {//}
The src/main/resources contains application-dev.yml properties file while there's a corresponding application.yml in src/test/resources.
application.yml (used in tests):
spring:
application:
name: daily-cash-report-application
#profiles: dev
sftp:
in:
host: eafdev
port: 22
user: eafdev
landingDir: C:\ABC #/opt/EAF/Data/Stage
tmpDir: /opt/EAF/ephub/tmp
localDir: /opt/EAF/ephub/tmp
fetchSize: 1
privateKey:
#privateKey: file:///C:/Users/x123345/AppData/Roaming/SSH/UserKeys/distributessh
chmod: 664
poller:
fixedDelay: 10000
out:
host: eafdev
port: 22
user: eafdev
stagingDir: C:\ABC #/opt/EAF/Data/Stage
tmpDir: /opt/EAF/ephub/tmp
privateKey:
#privateKey: file:///C:/Users/x12345/AppData/Roaming/SSH/UserKeys/distributessh
chmod: 664
file:
out:
stagingDir: C:\ABC #/opt/EAF/Data/Stage
tmpDir: C:\ABC
Doesn't @SpringJUnitConfig do the normal classpath scanning? What could be missing or misconfigured in this setup?
Supplying the 2 @Configuration classes to the new annotation (while leaving the ones with @Service, @Component, etc. as is:
@SpringJUnitConfig(classes= {FileAcquisitionConfig.class, FileDistributionConfig.class})
class DataMarshallerServiceTest {//}
results in another error:
Caused by: org.springframework.beans.TypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'int'; nested exception is java.lang.NumberFormatException: For input string: "${sftp.in.port}"
at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:79)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1339)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1311)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:659)
... 90 common frames omitted
whereas same properties resolve fine under @SpringBootTest.
Here's an excerpt from one of the configuration files above, containing the culprit ${sftp.in.host}, for reference:
@Slf4j
@Configuration
public class FileAcquisitionConfig {
public final static String RECEIVER_CHANNEL = "fromSftpChannel";
//sftp
@Value("${sftp.in.host}") @Getter
private String host;
@Value("${sftp.in.port}")
private int port;
@Value("${sftp.in.user}")
private String user;
//. . .
Another experiment per suggestion:
@SpringJUnitConfig(classes= {DcrDataFactoryApplication.class, FileAcquisitionConfig.class, FileDistributionConfig.class})
@TestPropertySource(locations = { "classpath:application.yml" })
//@SpringBootTest
class DataMarshallerServiceTest {
@Autowired
private DataMarshallerService cut;
results in the same error:
Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'sftp.out.host' in value "${sftp.out.host}"
After lots of experimentation, I've come up with the setup that seems to work. The assumptions which I was making to come up with the setup and to base my thought process on are described below.
Given there are many pieces to the setup and the fact that Spring Boot documentation never completely explains the rationale for having (or not having) certain attributes on certain annotations, but not on the others, as well as the inter-relationships and potential use cases for various possible combinations and permutations of different annotations' variants, this answer represents only what has worked for me at this point - feel free to contradict and challenge it, a clearer explanation will certainly benefit the community and is most welcome.
As described in the initial question, there seems to be an asymmetry between how @SpringBootTest and @SpringJUnitConfig with its various associated annotations handle reading of properties, in yaml format or otherwise, resolving dependencies (designated with @Config or other @Bean/@Service/@Component, etc. stereotypes), etc.
That's why, among other items of the classes attribute of the @SpringJUnitConfig I've added the DcrDataFactoryApplication.class to attempt to pull every scannable component on the classpath as the next two members there FileAcquisitionConfig.class and FileDistributionConfig.class, being @Configuration's themselves only contained the @Bean designanted types and clearly didn't make much of a difference:
@SpringJUnitConfig(classes= {DcrDataFactoryApplication.class, FileAcquisitionConfig.class, FileDistributionConfig.class})
That still didn't work, as described in the updates to the original question, so I started playing with the @TestPropertySource's members - having added just locations = { "classpath:application.yml" } didn't cut it, so I then noticed it allows for another attribute, properties, so I started to add different properties from the application.yml to it incrementally:
@TestPropertySource(
locations = { "classpath:application.yml" },
properties={"sftp.out.host=eafdev", "sftp.out.port=22"})
and that's when I started seeing a difference - whatever is added via properties gets resolved and overrides the same properties inside of application.yml + eliminating the Spring's irrelevant String to int coercion error message.
That seemed to indicate that, opposite to @SpringBootTest, the @TestPropertySource doesn't have the ability to process YAML files out of the box which prompted me to look in that direction.
Some blog indicated that, indeed, Spring Boot by default doesn't load YAML which is when I recalled that aside from @SpringBootApplication I earlier attached the following to my DcrDataFactoryApplication.class:
@Slf4j
@SpringBootApplication
@PropertySource(value = "classpath:feeds-config.yml", factory = com.pru.globalpayments.feeds.downstream.YamlPropertySourceFactory.class)
public class DcrDataFactoryApplication implements CommandLineRunner {//}
where YamlPropertySourceFactory.class is as follows, per above mentioned blog:
public class YamlPropertySourceFactory implements PropertySourceFactory {
@Override
public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource) throws IOException {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(encodedResource.getResource());
Properties properties = factory.getObject();
return new PropertiesPropertySource(encodedResource.getResource().getFilename(), properties);
}
}
With that discovery, I started looking at ways I can enable the testing infrastructure with that Yaml resolver, given that @TestPropertySource doesn't have a comparable factory attribute to pass that resolver in, as shown above, nor does the presence of DcrDataFactoryApplication on the @SpringJUnitConfig(classes= array, with its knowledge of how to resolve Yaml-formatted property files make any difference either.
That led me to downgrading the setup to using a@PropertySource instead of @TestPropertySource as the former does have an attribute to activate that linking to a factory:
@PropertySource(value = "application.yml", factory = YamlPropertySourceFactory.class)
That approach seems to be seconded by the discussions I found in another blog, or rather a thread on the Spring projects Github.
Plus it highlighted the need for yet another participant in this testing orgy, namely @EnableConfigurationProperties.
So, cutting the long story short, here's the final overall test setup that I've come up with:
@SpringJUnitConfig(classes= {DcrDataFactoryApplication.class, FileAcquisitionConfig.class, FileDistributionConfig.class})
@EnableConfigurationProperties
@PropertySource(value = "application.yml", factory = YamlPropertySourceFactory.class)
That allows me to completely remove @SpringBootTest dependency and replace it with more verbose, cryptic, but hopefully (per Spring documents) more targeted approach in our tests.
This approach has been tested on a couple of test classes and seems to hold the water so far.
Feel free to point out any shortcomings or if I'm inadvertently misinterpreting any of the initial Spring Boot's vision for such test setup.
I highlighted a couple of things in Spring Boot, that could potentially be either more documented / explained or improved.
Thank you for reading.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With