khmarbaise
khmarbaise

Reputation: 97517

Controller Test fails with MethodArgumentConversionNotSupportedException but not in the running application

Spring Boot App using 2.0.3.RELEASE of Spring Boot.

So I have REST API controller written like this:

@RestController
@RequestMapping("/root/{id}")
@Slf4j
public class RootController {

  @GetMapping
  public ResponseEntity<?> getXXX(
      @PathVariable String id,
      @RequestParam(value = "status") Status status, 
      @RequestParam(value = "comment") String comment,       
@RequestParam(value = "other") Optional<String> other) {
    log.info("Requested getXXX id={} status={} other={} comment={}", id, status, other, comment);

    return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
  }

}

So the interesting part is the Optional<String> other on the above definition. I have tested the above manually via calling curl:

curl -v -X GET 'http://localhost:8080/root/ID?status=OK&comment=Comment'

which results in logging output on console like this:

...Requested getXXX id=ID status=OK other=Optional.empty comment=Comment

and using curl like this:

curl -v -X GET 'http://localhost:8080/root/ID?status=OK&comment=Comment&other=MoreOther'

which results in the following output:

Requested getXXX id=ID status=OK other=Optional[MoreOther] comment=Comment

So far so good.

But of course I would like to check this via unit tests and not manually...So I wrote a REST controller test which looks like this:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = RootController.class)
@AutoConfigureMockMvc
public class RootControllerTest {

  @Autowired
  private MockMvc mvc;

  @Test
  public void shouldReturnNotImplemented() throws Exception {
    //@formatter:off
    mvc.perform(
        get("/root/xyz?status={status}&comment={comment}&other={other}", Status.NOTOK, "COMMENT", Optional.<String>of("Other"))
          .characterEncoding("UTF-8")
          .accept(MediaType.ALL)
      )
    .andExpect(
          status().isNotImplemented()
        );
    //@formatter:on
  }

But unfortunately the above test fails with:

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /root/xyz
       Parameters = {status=[OK], comment=[Comment], other=[Optional[Other]]}
          Headers = {Accept=[*/*]}
             Body = null
    Session Attrs = {}

Handler:
             Type = ...RootController
           Method = public org.springframework.http.ResponseEntity<?> .getRoot(java.lang.String,Status,java.lang.String,java.util.Optional<java.lang.String>)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = org.springframework.web.method.annotation.MethodArgumentConversionNotSupportedException

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 500
    Error message = null
          Headers = {}
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

Where the exception: org.springframework.web.method.annotation.MethodArgumentConversionNotSupportedException is the thing I don't understand.

The final question is: Why does the test fail but the running application does not? Does someone has a hint / idea for me?

Update 1:

I have tested also the following:

    get("/root/xyz?status={status}&comment={comment}&other={other}", Status.NOTOK, "COMMENT", "Other")

and furthermore which means using only strings.

    get("/root/xyz?status={status}&comment={comment}&other={other}", "NOTOK", "COMMENT", "Other")

The point that the running application works perfectly but unfortunately the test do not.

Update 2:

So after turning on debugging mode in test I got the following output: This brings me more and more into the direction that there is a bug within it..cause the parameters are always converted into String instead of Optional...and based on the parameters of get(..., Object... uriVars) it looks like there is some problem in the code...

2018-07-16 15:50:21.029 DEBUG 16022 --- [main] s.w.s.m.m.a.RequestMappingHandlerMapping : Looking up handler method for path /root/xyz
2018-07-16 15:50:21.031 DEBUG 16022 --- [main] s.w.s.m.m.a.RequestMappingHandlerMapping : Returning handler method [public org.springframework.http.ResponseEntity<?> de....RootController.getXXX(java.lang.String,de....Status,java.lang.String,java.util.Optional<java.lang.String>)]
2018-07-16 15:50:21.057 DEBUG 16022 --- [main] .w.s.m.m.a.ServletInvocableHandlerMethod : Failed to resolve argument 3 of type 'java.util.Optional'

org.springframework.web.method.annotation.MethodArgumentConversionNotSupportedException: Failed to convert value of type 'java.lang.String' to required type 'java.util.Optional'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'java.util.Optional': no matching editors or conversion strategy found
    at org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver.resolveArgument(AbstractNamedValueMethodArgumentResolver.java:127)
    at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:124)
    at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:161)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:131)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:877)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:783)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:991)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:866)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:635)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851)
    at org.springframework.test.web.servlet.TestDispatcherServlet.service(TestDispatcherServlet.java:71)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
    at org.springframework.mock.web.MockFilterChain$ServletFilterProxy.doFilter(MockFilterChain.java:166)
    at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:133)
    at org.springframework.test.web.servlet.MockMvc.perform(MockMvc.java:165)
    at de...RootControllerTest.shouldReturnNotImplemented(RootControllerTest.java:35)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:73)
    at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:83)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:538)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:760)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:460)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:206)
Caused by: java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'java.util.Optional': no matching editors or conversion strategy found
    at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:299)
    at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:99)
    at org.springframework.beans.TypeConverterSupport.doConvert(TypeConverterSupport.java:73)
    at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:52)
    at org.springframework.validation.DataBinder.convertIfNecessary(DataBinder.java:692)
    at org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver.resolveArgument(AbstractNamedValueMethodArgumentResolver.java:123)
    ... 50 common frames omitted

Upvotes: 3

Views: 1282

Answers (1)

Darren Forsythe
Darren Forsythe

Reputation: 11411

This is an issue with how you are loading the test up.

When you specify @SpringBootTest(classes = RootController.class) a class with SpringBootTest it will only load that class into the context i.e. it allows you to specify certain configurations etc. that you would want to test for some integration tests rather than using ContextConfiguration.

You can either remove the RootController and load a full test application context, effectively loading your entire application.

or just specify,

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

To load a slice test, which will only load the required beans to completely test the WebMVC.

working tests,

https://github.com/Flaw101/mockmvctests

Edit,

I've updated my example and introduced a second controller but only load the RootController in the RootControllerMock via @WebMvcTest(controllers = RootController.class). You can see in the logged output that it only load this controller.

2018-07-16 15:34:28.264 INFO 6176 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/root/{id}],methods=[GET]}" onto public org.springframework.http.ResponseEntity<?> com.darrenforsythe.mockmvc.RootController.getXXX(java.lang.String,java.lang.String,java.lang.String,java.util.Optional<java.lang.String>)

references,

https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTest.html

https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html#boot-features-testing-spring-boot-applications-testing-autoconfigured-mvc-tests

Upvotes: 0

Related Questions