Search code examples
spring-bootkotlinunit-testingtesting

Importing ConfiguartionProperties class into WebMvcTest finds configuration class but not the properties


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?


Solution

  • 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])