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 Converter
s:
@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?
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.