首页 > 解决方案 > 控制器测试失败并出现 MethodArgumentConversionNotSupportedException 但不在正在运行的应用程序中

问题描述

Spring Boot App 使用 2.0.3.RELEASE 的 Spring Boot。

所以我有这样写的 REST API 控制器:

@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);
  }

}

所以有趣的部分是Optional<String> other上面的定义。我已经通过调用 curl 手动测试了上述内容:

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

这导致在控制台上记录输出,如下所示:

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

并像这样使用卷曲:

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

这导致以下输出:

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

到目前为止,一切都很好。

但是我当然想通过单元测试而不是手动来检查这个……所以我写了一个 REST 控制器测试,它看起来像这样:

@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
  }

但不幸的是,上述测试失败了:

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 = []

例外在哪里:org.springframework.web.method.annotation.MethodArgumentConversionNotSupportedException是我不明白的事情。

最后一个问题是:为什么测试失败而运行的应用程序却没有?有人对我有提示/想法吗?

更新1:

我还测试了以下内容:

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

而且这意味着只使用字符串。

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

正在运行的应用程序可以完美运行,但不幸的是测试没有。

更新 2:

因此,在测试中打开调试模式后,我得到以下输出:这让我越来越倾向于其中存在错误的方向......因为参数总是转换为字符串而不是可选......并且基于它的参数get(..., Object... uriVars) 看起来代码中存在一些问题......

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

标签: restunit-testingspring-bootspring-boot-test

解决方案


这是您如何加载测试的问题。

当您使用它指定 @SpringBootTest(classes = RootController.class)一个类时,它只SpringBootTest会将该类加载到上下文中,即它允许您指定某些配置等,您希望为某些集成测试进行测试,而不是使用.ContextConfiguration

您可以删除RootController并加载完整的测试应用程序上下文,从而有效地加载整个应用程序。

或者只是指定,

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

加载一个切片测试,它将只加载所需的 bean 来完全测试 WebMVC。

工作测试,

https://github.com/Flaw101/mockmvctests

编辑,

我已经更新了我的示例并引入了第二个控制器,但只加载RootControllerRootControllerMockvia @WebMvcTest(controllers = RootController.class)。您可以在记录的输出中看到它仅加载此控制器。

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>)

参考,

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


推荐阅读