Steven Schlansker
Steven Schlansker

Reputation: 38526

Spring Boot test case doesn't use custom conversion service

I am trying to write up an integration test case with Spring Boot Test.

I customize the ConversionService to know about the new java.time types:

@Configuration
public class ConversionServiceConfiguration {
    @Bean
    public static ConversionService conversionService() {
        final FormattingConversionService reg = new DefaultFormattingConversionService();
        new DateTimeFormatterRegistrar().registerFormatters(reg);
        return reg;
    }
}

and then later expect it to work:

@Component
class MyServiceConfig {
    @Value("${max-watch-time:PT20s}")
    private Duration maxWatchTime = Duration.ofSeconds(20);
}

When running under the normal SpringApplication.run this seems to work fine. However, in my test case:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT, classes= {
    MyServiceMain.class,
    AttachClientRule.class
})
public class MyTest {
    @Inject
    @Rule
    public AttachClientRule client;

    @Test(expected=IllegalArgumentException.class)
    public void testBad() throws Exception {
        client.doSomethingIllegal();
    }
}

it blows up:

Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'AttachClientRule': Unsatisfied dependency expressed through constructor parameter 0:

Error creating bean with name 'MyServiceConfig': Unsatisfied dependency expressed through field 'maxWatchTime': Failed to convert value of type [java.lang.String] to required type [java.time.Duration];

nested exception is java.lang.IllegalStateException: Cannot convert value of type [java.lang.String] to required type [java.time.Duration]: no matching editors or conversion strategy found;

Peering deep into the guts of the TypeConverterDelegate that does the actual conversion, it seems to capture the ConversionService used from a field on the DefaultListableBeanFactory. Setting a watchpoint on where that field is set, I find the AbstractApplicationContext.refresh() method:

// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();  // <--- MyServiceConfig initialized here
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory); // <--- DefaultListableBeanFactory.conversionService set here!!!
// Last step: publish corresponding event.
finishRefresh();

So the @Value injection is happening before the ConversionService is applied to the BeanFactory. No bueno!

I've found what seems to be a workaround:

@Configuration
public class ConversionServiceConfiguration implements BeanFactoryPostProcessor {
    @Bean
    public static ConversionService conversionService() {
        final FormattingConversionService reg = new DefaultFormattingConversionService();
        new DateTimeFormatterRegistrar().registerFormatters(reg);
        return reg;
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        beanFactory.setConversionService(conversionService());
    }
}

This forces the initialization to happen earlier on, but doesn't feel like the right solution (at least it's not documented as such).

Where have I gone wrong? Spring 4.3.0, Spring Boot 1.4.0M3

EDIT

And now I've discovered another way for it to fail! Without making the same configuration class implement EnvironmentAware:

@Override
public void setEnvironment(Environment environment) {
    ((AbstractEnvironment) environment).setConversionService(conversionService());
}

I find that the PropertySourcesPropertyResolver uses the wrong (default) ConversionService. This is driving me mad!

Caused by: java.lang.IllegalArgumentException: Cannot convert value [PT15s] from source type [String] to target type [Duration] at org.springframework.core.env.PropertySourcesPropertyResolver.getProperty(PropertySourcesPropertyResolver.java:94) at org.springframework.core.env.PropertySourcesPropertyResolver.getProperty(PropertySourcesPropertyResolver.java:65) at org.springframework.core.env.AbstractPropertyResolver.getProperty(AbstractPropertyResolver.java:143) at org.springframework.core.env.AbstractEnvironment.getProperty(AbstractEnvironment.java:546) at com.mycorp.DoSomething.go(DoSomething.java:103)

Upvotes: 6

Views: 4475

Answers (2)

Steven Schlansker
Steven Schlansker

Reputation: 38526

The Spring Boot developers have confirmed that this is poorly documented and does not work as specified: https://github.com/spring-projects/spring-boot/issues/6222

Upvotes: 2

luboskrnac
luboskrnac

Reputation: 24561

Try to remove static keyword from conversionService bean definition.

Upvotes: 0

Related Questions