In order to help me diagnose failures of test (in particular when context fails to be initialized), I need to get a report such as the following to understand whether my @ConditionalOnProperty logic is correct.

@Configuration
@ConditionalOnProperty(value= DynamicCatalogConstants.OPT_IN_PROPERTY)
@EnableConfigurationProperties
public class DynamicCatalogServiceAutoConfiguration {
[...]
20-11-2020 14:47:02.380 [main] DEBUG o.s.b.a.l.ConditionEvaluationReportLoggingListener.logAutoConfigurationReport - 


============================
CONDITIONS EVALUATION REPORT
============================


Positive matches:
-----------------

   DynamicCatalogServiceAutoConfiguration matched:
      - @ConditionalOnProperty (osbcmdb.dynamic-catalog.enabled) matched (OnPropertyCondition)


Negative matches:
-----------------

    None


Exclusions:
-----------

    None


Unconditional classes:
----------------------

    None

I'm using spring-boot 2.3.6.

Observed behavior

The condition report is not invoked when context fails to start

Instead of a condition report, only the following context init exception is displayed

20-11-2020 14:24:10.609 [main] WARN  o.s.c.a.AnnotationConfigApplicationContext.refresh - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.support.BeanDefinitionOverrideException: Invalid bean definition with name 'dynamicCatalogService' defined in com.orange.oss.osbcmdb.catalog.DynamicCatalogServiceAutoConfigurationTest$SingleServiceDefinitionAnswerAutoConfig: Cannot register bean definition [Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=dynamicCatalogServiceAutoConfigurationTest.SingleServiceDefinitionAnswerAutoConfig; factoryMethodName=dynamicCatalogService; initMethodName=null; destroyMethodName=(inferred); defined in com.orange.oss.osbcmdb.catalog.DynamicCatalogServiceAutoConfigurationTest$SingleServiceDefinitionAnswerAutoConfig] for bean 'dynamicCatalogService': There is already [Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=dynamicCatalogServiceAutoConfiguration; factoryMethodName=dynamicCatalogService; initMethodName=null; destroyMethodName=(inferred); defined in com.orange.oss.osbcmdb.catalog.DynamicCatalogServiceAutoConfiguration] bound.


java.lang.IllegalStateException: Unstarted application context org.springframework.boot.test.context.assertj.AssertableApplicationContext[startupFailure=org.springframework.beans.factory.support.BeanDefinitionOverrideException] failed to start

    at org.springframework.boot.test.context.assertj.AssertProviderApplicationContextInvocationHandler.getStartedApplicationContext(AssertProviderApplicationContextInvocationHandler.java:156)
    at org.springframework.boot.test.context.assertj.AssertProviderApplicationContextInvocationHandler.invokeApplicationContextMethod(AssertProviderApplicationContextInvocationHandler.java:147)
    at org.springframework.boot.test.context.assertj.AssertProviderApplicationContextInvocationHandler.invoke(AssertProviderApplicationContextInvocationHandler.java:85)
    at com.sun.proxy.$Proxy20.getBean(Unknown Source)
    at com.orange.oss.osbcmdb.catalog.DynamicCatalogServiceAutoConfigurationTest.lambda$dynamicServiceLoadWhenOptInProperty$2(DynamicCatalogServiceAutoConfigurationTest.java:84)

Sample code:

    @Test
    void dynamicServiceLoadWhenOptInProperty() {
        ConditionEvaluationReportLoggingListener initializer = new ConditionEvaluationReportLoggingListener(LogLevel.INFO);
        ApplicationContextRunner contextRunner = new ApplicationContextRunner()
            .withInitializer(initializer)
            .withConfiguration(AutoConfigurations.of(
                SingleServiceDefinitionAnswerAutoConfig.class,
                DynamicCatalogServiceAutoConfiguration.class,
                MockedMaintenanceInfoFormatterServiceConfig.class
            ))
            .withPropertyValues(DynamicCatalogConstants.OPT_IN_PROPERTY + "=true");
        contextRunner.run(context -> {
            Catalog catalog = context.getBean(Catalog.class);
            assertThat(catalog.getServiceDefinitions()).isNotEmpty();

            assertThat(context).hasSingleBean(Catalog.class);
        });
    }

What I tried

The logging level is properly set to debug.

Stepping into the debugger, it seems that the ConditionEvaluationReportLoggingListener is never invoked for event notifications.

I however managed to manually trigger the condition report printed by invoking it explicitly with the debugger:

References

  • https://docs.spring.io/spring-boot/docs/2.3.6.RELEASE/reference/htmlsingle/#boot-features-test-autoconfig
  • https://stackoverflow.com/questions/64930752/why-conditionevaluationreportlogginglistener-never-invoked-in-failed-context-tes

Comment From: snicoll

Thanks for the report. The doc does not indicate that if the test fails the report is not going to be printed, unfortunately. It would be nice if we could alleviate this limitation so that it is provided regardless of the outcome of the test.

Comment From: wilkinsona

I'm not sure that there's anything that we can do here. ConditionEvaluationReportLoggingListener listens for an ApplicationFailedEvent that is published by and is quite strongly coupled to SpringApplication. When using ApplicationContextRunner there's no SpringApplication involved – an application context is created and refresh directly instead – so I don't think we could create and publish an ApplicationFailedEvent as its SpringApplication (and args) would both have to be null.

Comment From: wilkinsona

We discussed this one today and decided that we don't want to do anything to make it work out of the box. Should it be needed, you can roll your own using existing public API. Something like this:

    @Test
    void deliberateStartupFailure() {
        ConditionEvaluationReportPrinter printer = new ConditionEvaluationReportPrinter();
        this.contextRunner.withInitializer(printer).withUserConfiguration(FailureConfig.class).run((context) -> {
            if (context.getStartupFailure() != null) {
                printer.print();
            }
        });
    }

    static class ConditionEvaluationReportPrinter
            implements ApplicationContextInitializer<ConfigurableApplicationContext> {

        private ConditionEvaluationReport report;

        @Override
        public void initialize(ConfigurableApplicationContext applicationContext) {
            this.report = ConditionEvaluationReport.get(applicationContext.getBeanFactory());
        }

        void print() {
            System.out.println(new ConditionEvaluationReportMessage(this.report));
        }

    }

    @Configuration(proxyBeanMethods = false)
    static class FailureConfig {

        @Bean
        public String failure() {
            throw new RuntimeException();
        }

    }

This could be reduced quite a bit by adding some public API to ConditionEvaluationReportLoggingListener to manually trigger logging of the report but, given that we haven't seen much demand for this, it doesn't feel worth it. We can reconsider in the future if demand becomes apparent.