如何使用@PathVariable对Spring MVC控制器进行单元测试?

53

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

@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)
7个回答

47

根据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
    }
}

更多信息,我已经写了一篇关于测试Spring MVC注释的集成的博客文章:integration testing Spring MVC annotations


2
如果我的配置是在Java类中而不是XML文件中怎么办? - zygimantus

37

从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/


4
@Before中的代码为:this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); 其意思是使用当前的web应用程序上下文(this.wac)构建一个MockMvc实例(this.mockMvc)。 - Marcin Wasiluk
@MarcinWasiluk 感谢您的评论。是的,MockMVC是一个框架,正如链接文档所述,它在很大程度上依赖于静态导入以提高可读性,这就是为什么我在我的代码示例中省略了它。 - Clint Eastwood
9
好的,将它们添加在这里,这样其他人就不必再进行相同的挖掘了。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; - eis
2
而mimeType()在我的版本中实际上是contentType()。 - eis

10

1
这绝对是正确的方法 - 可惜它依赖于Spring 3.1。 - Otto Allmendinger
2
参见:https://jira.springsource.org/browse/SPR-9211 - 如果感兴趣请投票。 :-) - David Victor
非常感谢。我们的项目中有Spring 3.1.4。我想至少使用3.2(因为3.1.4不包含测试REST处理程序的内容)。也许在您指出的spring-test-mvc的帮助下,我可以完成我的工作。 - flaz14

3

我发现你可以手动将一个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
}

我肯定会对找到更好的选择感兴趣。


我看到你的方法最为简洁和完整。它对我来说非常顺利地运行了。 - Sym-Sym

3
异常消息指的是一个名为“feed”的变量,但在您的示例代码中并不存在。很可能是由于您没有展示给我们的内容引起的。
此外,您的测试既测试了Spring,也测试了您自己的代码。这真的是您想要做的吗?
更好的方法是假设Spring已经正常工作(它确实如此),只测试您自己的类,即直接调用MyController.doSomething()。这就是注释方法的好处之一——您不需要使用模拟请求和响应,只需使用域POJO即可。

抱歉,[feed] 是打错字了,应该是 [id]。在这个特定的测试中,我需要测试由 ViewResolvers 层次结构返回的视图。Spring 只有在正确配置的情况下才能正常工作... - martiner
这是正确的,但这也超出了单元测试的范围。在测试中使用AnnotationMethodHandlerAdapter并不能保证控制器在您的应用程序中真正工作。如果您想检查MVC行为,您需要编写功能测试(尝试使用HtmlUnit)。 - skaffman

3

如果您使用的是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。 - Amir Pashazadeh
如果您添加了spring-test-mvc maven构件,您可以使用Spring 3.2测试框架。如果您想使用我的答案,请参考这个回答http://stackoverflow.com/questions/12902247/spring-3-1-dependency-injection-junit-testing。 - Sym-Sym

1

我不确定我的原始答案是否适用于 @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
    }

我在使用Spring MVC注解进行集成测试方面发布了一篇新博客文章。


好的答案。我尝试了这种方法,但是我总是得到nullapplicationContext - fastcodejava

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接