Hello. I'm using spring boot version 2.2.6.RELEASE with Java 11. I've reproduced this bug in an example project here: https://github.com/Philosobyte/SpringBootExperiment

When @ConfigurationPropertiesScan is used to find configuration properties, and a test is annotated with @WebMvcTest, running the test will result in configuration properties not being found.

Using my example project, running com.philosobyte.springbootexperiment.ExperimentControllerTest results in the following:

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in com.philosobyte.springbootexperiment.ExperimentComponent required a bean of type 'com.philosobyte.springbootexperiment.ExperimentComponent$ExperimentalProperties' that could not be found.


Action:

Consider defining a bean of type 'com.philosobyte.springbootexperiment.ExperimentComponent$ExperimentalProperties' in your configuration.

All that is required for this message to go away is to remove the @WebMvcTest annotation. However, I believe this bug is not specific to this annotation. Performing debugging, I traced the path taken by scanning:

ConfigurationPropertiesScanRegistrar.registerBeanDefinitions line 60 ->
ConfigurationPropertiesScanRegistrar.scan line 83 ->
ClassPathScanningCandidateComponentProvider.findCandidateComponents line 315 ->
ClassPathScanningCandidateComponentProvider.scanCandidateComponents line 429 ->
ClassPathScanningCandidateComponentProvider.isCandidateComponent line 490 ->
TypeExcludeFilter.match line 66 ->
TypeExcludeFiltersContextCustomizer.createDelegatingTypeExcludeFilter line 98 ->
AnnotationCustomizableTypeExcludeFilter.match line 53 -> // this line is important
AnnotationCustomizableTypeExcludeFilter.include line 64 ->
AnnotationCustomizableTypeExcludeFilter.defaultInclude

The provider iterates through exclude filters to see if it should exclude my configuration properties class ExperimentalProperties from this scan. AnnotationCustomizableTypeExcludeFilter.match line 53 is where it makes its decision. include is returning false when comparing ExperimentalProperties against a filter delegate matching for WebMvcTest, thus causing the entire return statement to be false.

Here is a debug session at a previous point in time to show at a high level the objects being compared: spring_boot

I believe the logic at AnnotationCustomizableTypeExcludeFilter.match line 53 is flipped and instead of

return !(include(metadataReader, metadataReaderFactory) && !exclude(metadataReader, metadataReaderFactory));

It should read

return include(metadataReader, metadataReaderFactory) && !exclude(metadataReader, metadataReaderFactory);

To test my theory, I made this change to spring-boot locally, built it, switched over my personal project's version of spring boot to this local one, and ran my integration tests against it. The tests passed.

I would have made a PR for this but I wasn't sure if you would want a unit test or something, because I wouldn't know how to start writing a unit test for a class like this.

What do you think?

Comment From: philwebb

I think the logic is correct since AnnotationCustomizableTypeExcludeFilter is an exclude filter so a return of true indicates that the bean should be excluded.

The reason your tests fail is because @WebMvcTest intentionally limits the beans that it imports. From the javadoc:

Using this annotation will disable full auto-configuration and instead apply only configuration relevant to MVC tests (i.e. @Controller, @ControllerAdvice, @JsonComponent, Converter/GenericConverter, Filter, WebMvcConfigurer and HandlerMethodArgumentResolver beans but not @Component, @Service or @Repository beans).

I'm not sure we've considered how configuration property scanning will work with tests. Can you try adding @ConfigurationPropertiesScanRegistrar to ExperimentControllerTest to see if that works?

I'll flag this one for team attention to see if the rest of the team have any suggestions.

Comment From: mbhave

We discussed that in #16659. The outcome was that we decided to use the same TypeExcludeFilter for @ConfigurationPropertiesScan that is used for @ComponentScan. To include @ConfigurationProperties in slice tests they need to be explicitly imported like we do in this test.

Comment From: Philosobyte

I think the logic is correct since AnnotationCustomizableTypeExcludeFilter is an exclude filter so a return of true indicates that the bean should be excluded.

Ah, I see. I had originally interpreted include to mean "include as an item to be excluded by this filter" but it looks like it really means "include as a candidate in the ComponentProvider that is calling this filter".

I'm not sure we've considered how configuration property scanning will work with tests. Can you try adding @ConfigurationPropertiesScanRegistrar to ExperimentControllerTest to see if that works?

ConfigurationPropertiesScanRegistrar is not public, so I'm assuming you mean @ConfigurationPropertiesScan. I added that to the top of ExperimentControllerTest and nothing appeared to change. I think @ConfigurationPropertiesScan was already being picked up from being used in PropertiesConfig.java, which itself is marked as @Configuration. As to how well configuration property scanning works in tests - for me it works pretty well in every case besides this one.

The reason your tests fail is because @WebMvcTest intentionally limits the beans that it imports.

We discussed that in #16659. The outcome was that we decided to use the same TypeExcludeFilter for @ConfigurationPropertiesScan that is used for @ComponentScan. To include @ConfigurationProperties in slice tests they need to be explicitly imported like we do in this test.

This makes sense. Thank you for the explanation, and my bad for not reading the Javadocs on @WebMvcTest.

However, now knowing this, I still see something unexplained. Manually specifying @ConfigurationPropertiesScan does not override the TypeExcludeFilter in use during a slice test, right? Manually specifying @ComponentScan DOES appear to override the TypeExcludeFilter in use during a slice test. My original ExperimentController has a component dependency and so is not a good example of this. I pushed a new controller and a new test to my example repository to demonstrate:

// com.philosobyte.springbootexperiment.TrialController.java - simple controller
@RestController
public class TrialController {

    @GetMapping("/trial")
    public String trial() {
        return "trial";
    }
}

// com.philosobyte.springbootexperiment.TrialControllerTest.java - simple test telling WebMvcTest to only use TrialController.class
@WebMvcTest(controllers = TrialController.class)
public class TrialControllerTest {
    @Test
    public void testTrial() {

    }
}

// SpringbootexperimentApplication.java
@ComponentScan("com.philosobyte")
@ConfigurationPropertiesScan("com.philosobyte")
@SpringBootApplication
public class SpringbootexperimentApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootexperimentApplication.class, args);
    }

}

// com.philosobyte.springbootexperiment.ExperimentComponent.java 
@Component
@RequiredArgsConstructor
public class ExperimentComponent {
    private final ExperimentalProperties properties;

    public String getSubProperty() {
        return properties.getProperty();
    }

    @Data
    @ConfigurationProperties(prefix = "experiment")
    public static class ExperimentalProperties {
        private String property = "lol";
    }
}

Despite there being no explicit import of either ExperimentComponent or ExperimentalProperties, running TrialControllerTest fails to find com.philosobyte.springbootexperiment.ExperimentComponent$ExperimentalProperties because ExperimentComponent is being scanned while ExperimentalProperties is not.

Removing @ComponentScan("com.philosobyte") from SpringbootexperimentApplication resolves this issue and results in neither being scanned, as expected.

Comment From: snicoll

I've reopened it as I'd like to give that one an extra look with recent changes. Unless you've closed it because it's working now for you @Philosobyte?

Comment From: snicoll

Closing this one as https://github.com/spring-projects/spring-boot/issues/21216#issuecomment-620918857 feels the right outcome to me. If I missed something, please add a comment and we can reconsider.

Comment From: EugeneMsv

I think the logic is correct since AnnotationCustomizableTypeExcludeFilter is an exclude filter so a return of true indicates that the bean should be excluded.

Ah, I see. I had originally interpreted include to mean "include as an item to be excluded by this filter" but it looks like it really means "include as a candidate in the ComponentProvider that is calling this filter".

I'm not sure we've considered how configuration property scanning will work with tests. Can you try adding @ConfigurationPropertiesScanRegistrar to ExperimentControllerTest to see if that works?

ConfigurationPropertiesScanRegistrar is not public, so I'm assuming you mean @ConfigurationPropertiesScan. I added that to the top of ExperimentControllerTest and nothing appeared to change. I think @ConfigurationPropertiesScan was already being picked up from being used in PropertiesConfig.java, which itself is marked as @Configuration. As to how well configuration property scanning works in tests - for me it works pretty well in every case besides this one.

The reason your tests fail is because @WebMvcTest intentionally limits the beans that it imports.

We discussed that in #16659. The outcome was that we decided to use the same TypeExcludeFilter for @ConfigurationPropertiesScan that is used for @ComponentScan. To include @ConfigurationProperties in slice tests they need to be explicitly imported like we do in this test.

This makes sense. Thank you for the explanation, and my bad for not reading the Javadocs on @WebMvcTest.

However, now knowing this, I still see something unexplained. Manually specifying @ConfigurationPropertiesScan does not override the TypeExcludeFilter in use during a slice test, right? Manually specifying @ComponentScan DOES appear to override the TypeExcludeFilter in use during a slice test. My original ExperimentController has a component dependency and so is not a good example of this. I pushed a new controller and a new test to my example repository to demonstrate:

```java // com.philosobyte.springbootexperiment.TrialController.java - simple controller @RestController public class TrialController {

@GetMapping("/trial")
public String trial() {
    return "trial";
}

}

// com.philosobyte.springbootexperiment.TrialControllerTest.java - simple test telling WebMvcTest to only use TrialController.class @WebMvcTest(controllers = TrialController.class) public class TrialControllerTest { @Test public void testTrial() {

}

}

// SpringbootexperimentApplication.java @ComponentScan("com.philosobyte") @ConfigurationPropertiesScan("com.philosobyte") @SpringBootApplication public class SpringbootexperimentApplication {

public static void main(String[] args) {
    SpringApplication.run(SpringbootexperimentApplication.class, args);
}

}

// com.philosobyte.springbootexperiment.ExperimentComponent.java @Component @RequiredArgsConstructor public class ExperimentComponent { private final ExperimentalProperties properties;

public String getSubProperty() {
    return properties.getProperty();
}

@Data
@ConfigurationProperties(prefix = "experiment")
public static class ExperimentalProperties {
    private String property = "lol";
}

} ```

Despite there being no explicit import of either ExperimentComponent or ExperimentalProperties, running TrialControllerTest fails to find com.philosobyte.springbootexperiment.ExperimentComponent$ExperimentalProperties because ExperimentComponent is being scanned while ExperimentalProperties is not.

Removing @ComponentScan("com.philosobyte") from SpringbootexperimentApplication resolves this issue and results in neither being scanned, as expected.

I agree with this one, testing TrialControllerTest with help of @WebMvcTest(controllers = TrialController.class) goes against the documentation, where the parameter controllers says: Specifies the controllers to test. In reality the context is trying to add all controllers to the context. Adding exclude filters to ignore other packages/controllers doesn't work.

Comment From: wilkinsona

If you declare @ComponentScan and don't configure it with Boot's TypeExcludeFilter, the ability to control what's included in a sliced test is lost. This is mentioned in the reference documentation:

If you directly use @ComponentScan (that is, not through @SpringBootApplication) you need to register the TypeExcludeFilter with it. See the Javadoc for details.

If this doesn't help and you would like us to investigate further, please open a new issue.

Comment From: EugeneMsv

If you declare @ComponentScan and don't configure it with Boot's TypeExcludeFilter, the ability to control what's included in a sliced test is lost. This is mentioned in the reference documentation:

If you directly use @ComponentScan (that is, not through @SpringBootApplication) you need to register the TypeExcludeFilter with it. See the Javadoc for details.

If this doesn't help and you would like us to investigate further, please open a new issue.

In my particular case, it seems to be helping. thank you @wilkinsona