Reputation: 47
I am trying to write a simple @WebMvcTest in Kotlin with Spring Boot. The controller under Test depends on a Configuration class that is annotated with @ConfigurationProperties to pull one simple string property from my application-*.yml files. I have an application-test.yml file that defines this property and annotated the test class with @ActiveProfile("test") to use this property. Because @WebMvcTest is a slice that wouldn't load my configuration by default, I also added @Import(ApplicationConfiguration::class).
The test code now fails when trying to construct the ApplicationConfiguration Bean because it can't find the property. The regular application works fine and finds the properties by @ConfigurationPropertiesScan.
I've decluttered my code and changed some names before posting, but the following code still fails in the same way.
This is the bottom of the stacktrace:
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'com.example.vpsxprintproxy.configuration.ApplicationConfiguration': Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type 'java.lang.String' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:245)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1352)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1189)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:560)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:520)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200)
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1417)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1337)
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:888)
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
... 96 common frames omitted
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'java.lang.String' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1824)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1383)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1337)
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:888)
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
... 110 common frames omitted
The relevant classes:
@WebMvcTest(StatusController::class)
@Import(ApplicationConfiguration::class)
@ActiveProfiles("test")
class StatusControllerTest {
@Autowired
private lateinit var mockMvc: MockMvc
@Test
fun getStatus() {
mockMvc.perform(MockMvcRequestBuilders.get("/status"))
.andExpect(MockMvcResultMatchers.status().isOk)
.andExpect(
MockMvcResultMatchers.content().string("Application version someApplicationVersion is up and running.")
)
}
}
@RestController
@RequestMapping("/status")
class StatusController(
private val applicationConfiguration: ApplicationConfiguration
) {
@GetMapping
fun getStatus(): String = "Application version ${applicationConfiguration.version} is up and running."
}
@ConfigurationProperties(prefix = "application")
data class ApplicationConfiguration(
val version: String
)
And the application-test.yml file:
application:
version: someApplicationVersion
One more thing I've tried that leads to quirky behavior: I've changed my ApplicationConfiguration to have a default value like so
@ConfigurationProperties(prefix = "application")
data class ApplicationConfiguration(
var version: String = "default"
)
When I do this, the test not only is successful in finding some value for version; it finds the correct value from application-test.yml.
I am aware I can define my own test configuration class and/or mock this to have this specific test run through, but this would be cumbersome in classes with many properties and I'm rather curious why this behavior exists and how I can use my application-test.yml with @WebMvcTest. I also do not want to make this a @SpringBootTest because the application could become large and I don't want to start up the whole context.
Can someone explain this behavior and/or how to make my ApplicationConfiguration intantiate the properties from application-test.yml?
Upvotes: 3
Views: 707
Reputation: 126
This is actually a common problem that happens with slice tests and is also excerbated with changing behaviour between major Spring Boot versions.
I think that at some point slice tests (e.g. @WebMvcTest, @DataJpaTest...) did actually imported @ConfigurationProperties bean into slice context but I cannot find anything to back my claim.
Spring documentation states:
Regular @Component and @ConfigurationProperties beans are not scanned when the @WebMvcTest annotation is used. @EnableConfigurationProperties can be used to include @ConfigurationProperties beans.
In your case you should import your properties bean in explicit way, should be put on StatusControllerTest:
@EnableConfigurationProperties([ApplicationConfiguration::class])
Upvotes: 3