【问题标题】:How to unit test a Spring MVC controller using @PathVariable?如何使用@PathVariable 对 Spring MVC 控制器进行单元测试?
【发布时间】:2010-11-26 22:22:41
【问题描述】:

我有一个类似于这个的简单带注释的控制器:

@Controller
public class MyController {
  @RequestMapping("/{id}.html")
  public String doSomething(@PathVariable String id, Model model) {
    // do something
    return "view";
  }
}

我想用这样的单元测试来测试它:

public class MyControllerTest {
  @Test
  public void test() {
    MockHttpServletRequest request = new MockHttpServletRequest();
    request.setRequestURI("/test.html");
    new AnnotationMethodHandlerAdapter()
      .handle(request, new MockHttpServletResponse(), new MyController());
    // assert something
  }
}

问题是AnnotationMethodHandlerAdapter.handler()方法抛出异常:

java.lang.IllegalStateException: Could not find @PathVariable [id] in @RequestMapping
at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter$ServletHandlerMethodInvoker.resolvePathVariable(AnnotationMethodHandlerAdapter.java:642)
at org.springframework.web.bind.annotation.support.HandlerMethodInvoker.resolvePathVariable(HandlerMethodInvoker.java:514)
at org.springframework.web.bind.annotation.support.HandlerMethodInvoker.resolveHandlerArguments(HandlerMethodInvoker.java:262)
at org.springframework.web.bind.annotation.support.HandlerMethodInvoker.invokeHandlerMethod(HandlerMethodInvoker.java:146)

【问题讨论】:

    标签: java spring unit-testing spring-mvc spring-test


    【解决方案1】:

    如果您使用的是 Spring 3.0.x。

    在这里,我建议使用 spring-test 而不是 spring-test-mvc 合并 Emil 和 Scarba05 答案。如果您使用的是 Spring 3.2.x 或更高版本,请跳过此答案并参考 spring-test-mvc 示例

    MyControllerWithParameter.java

    @Controller
    public class MyControllerWithParameter {
    @RequestMapping("/testUrl/{pathVar}/some.html")
    public String passOnePathVar(@PathVariable String pathVar, ModelMap model){
        model.addAttribute("SomeModelAttribute",pathVar);
        return "viewName";
    }
    }
    

    MyControllerTest.java

    import static org.springframework.test.web.ModelAndViewAssert.assertViewName;
    import java.util.HashMap;
    import org.junit.Before;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.ApplicationContext;
    import org.springframework.mock.web.MockHttpServletRequest;
    import org.springframework.mock.web.MockHttpServletResponse;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    import org.springframework.test.web.ModelAndViewAssert;
    import org.springframework.web.servlet.HandlerAdapter;
    import org.springframework.web.servlet.HandlerMapping;
    import org.springframework.web.servlet.ModelAndView;
    import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = 
        {"file:src\\main\\webapp\\WEB-INF\\spring\\services\\servlet-context.xml" 
        })
    public class MyControllerTest {
    
    private MockHttpServletRequest request;
    private MockHttpServletResponse response;
    private HandlerAdapter handlerAdapter;
    
    @Before
    public void setUp() throws Exception {
        request = new MockHttpServletRequest();
        response = new MockHttpServletResponse();
        this.handlerAdapter = applicationContext.getBean(AnnotationMethodHandlerAdapter.class);
    }
    
    //  Container beans
    private MyControllerWithParameter myController;
    private ApplicationContext applicationContext;
    public ApplicationContext getApplicationContext() {
        return applicationContext;
    }
    @Autowired
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }
    public MyControllerWithParameter getMyController() {
        return myController;
    }
    @Autowired
    public void setMyController(MyControllerWithParameter myController) {
        this.myController = myController;
    }
    
    @Test
    public void test() throws Exception {
        request.setRequestURI("/testUrl/Irrelavant_Value/some.html");
        HashMap<String, String> pathvars = new HashMap<String, String>();
        // Populate the pathVariable-value pair in a local map
        pathvars.put("pathVar", "Path_Var_Value");
        // Assign the local map to the request attribute concerned with the handler mapping 
        request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, pathvars);
    
        final ModelAndView modelAndView = this.handlerAdapter.handle(request, response, myController);
    
        ModelAndViewAssert.assertAndReturnModelAttributeOfType(modelAndView, "SomeModelAttribute", String.class);
        ModelAndViewAssert.assertModelAttributeValue(modelAndView, "SomeModelAttribute", "Path_Var_Value");
        ModelAndViewAssert.assertViewName(modelAndView, "viewName");
    }
    

    }

    【讨论】:

    • 能否附上servlet-context.xml的来源。我们使用的是 Spring 3.1.x,我不知道如何配置 Spring 进行测试。
    • 如果你添加了 spring-test-mvc maven 工件,你可以使用 Spring 3.2 测试框架。如果你想用我的答案,请参考答案stackoverflow.com/questions/12902247/…
    【解决方案2】:

    从 Spring 3.2 开始,有一种适当的方法可以以一种优雅而简单的方式进行测试。您将能够执行以下操作:

    @RunWith(SpringJUnit4ClassRunner.class)
    @WebAppConfiguration
    @ContextConfiguration("servlet-context.xml")
    public class SampleTests {
    
      @Autowired
      private WebApplicationContext wac;
    
      private MockMvc mockMvc;
    
      @Before
      public void setup() {
        this.mockMvc = webAppContextSetup(this.wac).build();
      }
    
      @Test
      public void getFoo() throws Exception {
        this.mockMvc.perform(get("/foo").accept("application/json"))
            .andExpect(status().isOk())
            .andExpect(content().mimeType("application/json"))
            .andExpect(jsonPath("$.name").value("Lee"));
      }
    }
    

    欲了解更多信息,请查看http://blog.springsource.org/2012/11/12/spring-framework-3-2-rc1-spring-mvc-test-framework/

    【讨论】:

    • in @Before this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
    • @MarcinWasiluk 感谢您的评论。是的,MockMVC 是一个框架,正如链接文档所述,它严重依赖静态导入来提高可读性,这就是我在代码示例中省略它的原因。
    • 好吧,把它们加在这里,这样其他人就不必做同样的挖掘了。 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;
    • 和 mimeType() 实际上是 contentType(),至少在我的版本中是这样。
    【解决方案3】:

    一个很有前途的 Spring MVC 测试框架 https://github.com/SpringSource/spring-test-mvc

    【讨论】:

    • 这绝对是要走的路——糟糕的是它取决于 Spring 3.1
    • 另请参阅:jira.springsource.org/browse/SPR-9211 - 如果感兴趣,请投票。 :-)
    • 非常感谢。我们的项目中有 Spring 3.1.4。我想至少使用3.2(因为3.1.4 不包含用于测试REST 处理程序的东西)。也许在你指出的 spring-test-mvc 的帮助下我可以完成我的工作。
    【解决方案4】:

    我不确定我的原始答案是否会对@PathVariable 有所帮助。我刚刚尝试测试一个@PathVariable,但出现以下异常:

    org.springframework.web.bind.annotation.support.HandlerMethodInvocationException: 调用处理程序方法失败 [public org.springframework.web.servlet.ModelAndView test.MyClass.myMethod(test.SomeType)];嵌套异常是 java.lang.IllegalStateException:在 @RequestMapping 中找不到 @PathVariable [parameterName]

    原因是请求中的路径变量被拦截器解析。以下方法对我有用:

    import static org.springframework.test.web.ModelAndViewAssert.*;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration({"file:web/WEB-INF/application-context.xml",
            "file:web/WEB-INF/dispatcher-servlet.xml"})    
    public class MyControllerIntegrationTest {
    
        @Inject
        private ApplicationContext applicationContext;
    
        private MockHttpServletRequest request;
        private MockHttpServletResponse response;
        private HandlerAdapter handlerAdapter;
    
        @Before
        public void setUp() throws Exception {
            this.request = new MockHttpServletRequest();
            this.response = new MockHttpServletResponse();
    
            this.handlerAdapter = applicationContext.getBean(HandlerAdapter.class);
        }
    
        ModelAndView handle(HttpServletRequest request, HttpServletResponse response)
                throws Exception {
            final HandlerMapping handlerMapping = applicationContext.getBean(HandlerMapping.class);
            final HandlerExecutionChain handler = handlerMapping.getHandler(request);
            assertNotNull("No handler found for request, check you request mapping", handler);
    
            final Object controller = handler.getHandler();
            // if you want to override any injected attributes do it here
    
            final HandlerInterceptor[] interceptors =
                handlerMapping.getHandler(request).getInterceptors();
            for (HandlerInterceptor interceptor : interceptors) {
                final boolean carryOn = interceptor.preHandle(request, response, controller);
                if (!carryOn) {
                    return null;
                }
            }
    
            final ModelAndView mav = handlerAdapter.handle(request, response, controller);
            return mav;
        }
    
        @Test
        public void testDoSomething() throws Exception {
            request.setRequestURI("/test.html");
            request.setMethod("GET");
            final ModelAndView mav = handle(request, response);
            assertViewName(mav, "view");
            // assert something else
        }
    

    我在integration testing spring mvc annotations添加了一篇新博文

    【讨论】:

    • 好答案。我尝试了这种方法,但我总是得到null for applicationContext
    【解决方案5】:

    我发现您可以手动将 PathVariable 映射插入到请求对象中。这显然是不理想的,但似乎有效。在您的示例中,类似于:

    @Test
    public void test() {
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setRequestURI("/test.html");
        HashMap<String, String> pathvars = new HashMap<String, String>();
        pathvars.put("id", "test");
        request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, pathvars);
        new AnnotationMethodHandlerAdapter().handle(request, new MockHttpServletResponse(), new MyController());
       // assert something
    }
    

    我肯定有兴趣找到更好的选择。

    【讨论】:

    • 我看到你的方法是最简洁和完整的。它对我来说就像微风一样。
    【解决方案6】:

    在基于 Spring 参考手册中的术语进行集成测试之后,我会称您为什么。不如做这样的事情:

    import static org.springframework.test.web.ModelAndViewAssert.*;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration({/* include live config here
        e.g. "file:web/WEB-INF/application-context.xml",
        "file:web/WEB-INF/dispatcher-servlet.xml" */})
    public class MyControllerIntegrationTest {
    
        @Inject
        private ApplicationContext applicationContext;
    
        private MockHttpServletRequest request;
        private MockHttpServletResponse response;
        private HandlerAdapter handlerAdapter;
        private MyController controller;
    
        @Before
        public void setUp() {
           request = new MockHttpServletRequest();
           response = new MockHttpServletResponse();
           handlerAdapter = applicationContext.getBean(HandlerAdapter.class);
           // I could get the controller from the context here
           controller = new MyController();
        }
    
        @Test
        public void testDoSomething() throws Exception {
           request.setRequestURI("/test.html");
           final ModelAndView mav = handlerAdapter.handle(request, response, 
               controller);
           assertViewName(mav, "view");
           // assert something
        }
    }
    

    有关更多信息,我已经写了blog entry about integration testing Spring MVC annotations

    【讨论】:

    • 如果我的配置在 Java 类中而不是在 xml 文件中怎么办?
    【解决方案7】:

    异常消息指的是一个“feed”变量,它不在您的示例代码中,它可能是由您未向我们展示的某些内容引起的。

    此外,您的测试正在测试 Spring 您自己的代码。这真的是你想做的吗?

    最好假设 Spring 可以工作(确实如此),然后测试您自己的类,即直接调用 MyController.doSomething()。这是注释方法的一个好处 - 您不需要使用模拟请求和响应,您只需使用域 POJO。

    【讨论】:

    • 抱歉,[feed] 是错字,应该改为 [id]。在这个特定的测试中,我需要测试 ViewResolvers 层次结构返回的视图。 Spring 有效,但仅在正确配置时...
    • 没错,但这也超出了单元测试的范围。在您的测试中使用 AnnotationMethodHandlerAdapter 并不能保证控制器将在您的应用程序中真正工作。如果你想检查你的 MVC 行为,你需要编写功能测试(试试 HtmlUnit)。
    猜你喜欢
    • 2014-02-08
    • 1970-01-01
    • 2011-08-12
    • 2012-05-25
    • 1970-01-01
    • 2019-07-14
    • 2014-05-27
    • 2019-07-07
    • 1970-01-01
    相关资源
    最近更新 更多