How can I use @WebMvcTest for a Controller that uses an autowired ConversionService?

DaveyDaveDave picture DaveyDaveDave · Jan 18, 2018 · Viewed 7.9k times · Source

In a Spring Boot application, I have two POJOs, Foo and Bar, and a BarToFooConverter, which looks like:

@Component
public class BarToFooConverter implements Converter<Bar, Foo> {
    @Override
    public Foo convert(Bar bar) {
        return new Foo(bar.getBar());
    }
}

I also have a controller which makes use of the converter:

@RestController("test")
public class TestController {
    @Autowired
    private ConversionService conversionService;

    @RequestMapping(method = RequestMethod.PUT)
    @ResponseBody
    public Foo put(@RequestBody Bar bar) {
        return conversionService.convert(bar, Foo.class);
    }
}

I'd like to test this controller with @WebMvcTest, something like:

@WebMvcTest
@RunWith(SpringRunner.class)
public class TestControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Test
    public void test() throws Exception {
        mockMvc.perform(
                put("/test")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"bar\":\"test\"}"))
                .andExpect(status().isOk());
    }
}

but when I run this, I find that my BarToFooConverter was not registered with the ConversionService:

Caused by: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [com.example.demo.web.Bar] to type [com.example.demo.web.Foo]
    at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:324)
    at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:206)
    at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:187)
    at com.example.demo.web.TestController.put(TestController.java:15)

This seems to make sense, because, according to the Javadoc:

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

However, the reference guide differs slightly, saying that @WebMvcTest does include Converters:

@WebMvcTest auto-configures the Spring MVC infrastructure and limits scanned beans to @Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, WebMvcConfigurer, and HandlerMethodArgumentResolver. Regular @Component beans are not scanned when using this annotation.

It seems that the reference guide is incorrect here - or am I registering my Converter incorrectly?

I have also tried mocking the ConversionService in my test with:

@WebMvcTest
@RunWith(SpringRunner.class)
public class TestControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private ConversionService conversionService;

    @Test
    public void test() throws Exception {
        when(conversionService.convert(any(Bar.class), eq(Foo.class))).thenReturn(new Foo("test"));

        mockMvc.perform(
                put("/test")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"bar\":\"test\"}"))
                .andExpect(status().isOk());
    }
}

but now Spring complains that my mock ConversionService is overriding the default one:

Caused by: java.lang.IllegalStateException: @Bean method WebMvcConfigurationSupport.mvcConversionService called as a bean reference for type [org.springframework.format.support.FormattingConversionService] but overridden by non-compatible bean instance of type [org.springframework.core.convert.ConversionService$$EnhancerByMockitoWithCGLIB$$da4e303a]. Overriding bean of same name declared in: null
    at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.obtainBeanInstanceFromFactory(ConfigurationClassEnhancer.java:402)
    at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:361)
    ...

Ideally I'd like to use my original approach, with the real Converter in my test rather than mocking the ConversionService, but with @WebMvcTest to limit the scope of the components that are started, so I also tried using an includeFilter in the @WebMvcTest annotation:

@WebMvcTest(includeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com.example.demo.web.Bar*"))

but it still fails with the original 'No converter found capable of converting...' error message.

This feels like something that must be quite a common requirement - what am I missing?

Answer

Szymon Stepniak picture Szymon Stepniak · Jan 18, 2018

You can register your converter manually in @Before annotated method. All you have to do is to inject GenericConversionService and call addConverter(new BarToFooConverter()) to make the converter resolvable. In this case you can get rid of mocking part. Your test could look like this:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest
@RunWith(SpringRunner.class)
public class TestControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private GenericConversionService conversionService;

    @Before
    public void setup() {
        conversionService.addConverter(new BarToFooConverter());
    }

    @Test
    public void test() throws Exception {
        mockMvc.perform(
                put("/test")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"bar\":\"test\"}"))
                .andExpect(status().isOk());
    }
}

Alternative solution: Spring Boot since version 1.4.0 provides a collection of test-related auto-configurations and one of these auto-configurations is @AutoConfigureMockMvc that configures MockMvc component that works fine with injected converter components.