其他
测试
有两个很重要的测试时单元测试和集成测试,通常是从单元测试开始测试类中的单个方法,然后进行集成测试,以测试不同的模块是否可以无缝协同工作。
单元测试
我们应该对控制器中的每个方法进行单元测试。但是测试Controller有一些要求,因为它们通常与Servlet API对象(如HttpServletRequest,HttpServletResponse,HttpSession等)交互。我们需要模拟这些对象以正确测试控制器。Spring-Test模拟对象是专门为使用Spring而构建的,并且与真实环境更接近,更容易使用。当然,我们需要先导入spring-test.jar这个jar。
当调用控制器时,你可能需要传递HttpServletRequest和HttpServletResponse。在生产环境中,这两个的对象都有Servlet容器本身提供。在测试环境中,你可以使用org.springframework.mock.web包下的Spring MockHttpServletRequest和MockHttpServletResponse类:
MockHttpServletRequest request=new MockHttpServletRequest(); MockHttpServletResponse response=new MockHttpServletResponse();
MockHttpServletRequet类实现了javax.servlet.http.HttpServletRequest,并允许你将实例配置的看起来像一个真正的HttpServletRequest。它提供了方法来设置HttpServletRequest中的所有属性以及获取其属性值:
方法 | 描述 |
setRequestURI | 设置请求URI |
setParameter | 设置一个参数值 |
setMethod | 设置HTTP方法 |
set/getAttribute | 设置/获得一个属性 |
addHeader | 添加一个请求头 |
addParameter | 添加一个请求参数 |
getCookies | 获得所有的Cookies |
getContextPath | 返回上下文 |
MockHttpServletResponse实现了javax.servlet.http.HttpServletResponse,并提供了配置方法:
方法 | 描述 |
addCookie | 添加一个Cookie |
addHeader | 添加一个·HTTP响应头 |
getContentLength | 返回内容长度 |
getWriter | 返回Writer |
getOutputStream | 返回ServletOutputStream |
例子:
我们先来写一个简单的Controller,它拿到一个名为id的请求参数,之后进行一些处理:
/** * 单元测试SpringMVC */ @RequestMapping("/testMock") public String testMock(HttpServletRequest request,HttpServletResponse response) { Integer id=(Integer) request.getAttribute("id"); if(id==null) { response.setStatus(500); }else if(id==1) { request.setAttribute("viewed", 100); }else if(id==2) { request.setAttribute("viewed", 200); } return "mockView"; }
若请求属性id存在且值为1或2,则添加请求属性“viewed”,否则,不添加请求属性
我们写两个测试方法进行测试,一个传递id值作为正向测试,一个不传id值作为反向测试:
@Test public void testHelloWorldMockMethod() { HelloWorld world=new HelloWorld(); MockHttpServletRequest request=new MockHttpServletRequest(); MockHttpServletResponse response=new MockHttpServletResponse(); request.setRequestURI("/testMock"); request.setAttribute("id", 1); world.testMock(request, response); assertEquals(200, response.getStatus()); assertEquals(100, (int)request.getAttribute("viewed")); } @Test public void testHelloWorldNotAttributeMethod() { HelloWorld world=new HelloWorld(); MockHttpServletRequest request=new MockHttpServletRequest(); MockHttpServletResponse response=new MockHttpServletResponse(); request.setRequestURI("/testMock"); world.testMock(request, response); assertEquals(500, response.getStatus()); assertNull(request.getAttribute("viewed")); }
HelloWord就是Controller类,我们先初始化这个Controller,然后,Mock出两个对象,一个MockHttpServlet和一个MockHttpServletResponse。前者用于设置请求URL,并向MockHttpServletRequest添加一个属性“id”。之后调用Controller中的testMock方法,传递这两个Mock对象,最后验证响应状态吗是否为200,请求参数“viewed”的值。
ModelAndViewAssert
ModelAndViewAssert类是org.springframework.web.servlet包的一部分,是另一个有用的Spring类,用于测试控制器的请求处理方法返回的ModelAndView。ModelAndView是请求处理方法可以返回的类型之一,是包含有关请求处理方法的模型和视图的一个bean。
ModelAndViewAssert的一些主要方法:
方法 | 描述 |
assertViewName | 检查ModelAndView的视图名是否与预期名称匹配 |
assertModelAttributeAvailable | 检查ModelAndView的模型是否包括具有预期模型名称的属性 |
assertModelAttributeValue | 检查ModelAndView模型是否包含具有指定名称和值的属性 |
assertSortAndCompareListModelAttribute | 对ModelAndView的列表进行排序,肉厚将其与预期列表进行比较 |
例子:
一个返回ModelAndView的请求处理方法:
@RequestMapping("/latest/{pubYear}") public ModelAndView getLatestTitlees(@PathVariable String pubYear) { ModelAndView mv=new ModelAndView("Latest Titles"); if("2018".equals(pubYear)) { List<Book> list=Arrays.asList( new Book("0001", "SpringMVC :A Tutorial", "Paul Deck", LocalDate.of(2018, 1, 1)), new Book("0002", "Java Tutorial", "Budi Hurniawan", LocalDate.of(2018, 1, 16)), new Book("0003", "SQL", "Will Biteman", LocalDate.of(2018, 1, 30)) ); mv.addObject("latest", list); } return mv; }
其对应的测试方法:
@Test public void test() { BookController bookController=new BookController(); ModelAndView mv=bookController.getLatestTitlees("2018"); ModelAndViewAssert.assertViewName(mv, "Latest Titles"); ModelAndViewAssert.assertModelAttributeAvailable(mv, "latest"); List<Book> list=Arrays.asList( new Book("0001", "SpringMVC :A Tutorial", "Paul Deck", LocalDate.of(2018, 1, 1)), new Book("0002", "Java Tutorial", "Budi Hurniawan", LocalDate.of(2018, 1, 16)), new Book("0003", "SQL", "Will Biteman", LocalDate.of(2018, 1, 30)) ); ModelAndViewAssert.assertAndReturnModelAttributeOfType(mv, "latest", list.getClass()); Comparator<Book> pubDateComparator=(a,b)->a.getPubDate().compareTo(b.getPubDate()); ModelAndViewAssert.assertSortAndCompareListModelAttribute(mv, "latest", list, pubDateComparator); }
先是初始化Controller并调用其请求处理方法,这个请求处理方法当然是返回ModelAndView了,我们使用ModelAndViewAssert的静态方法进行测试即可。
集成测试
Spring的MockHttpServletRequest,MockHttpServletResponse和ModelAndViewAssert类适用于SpringMVC控制器进行单元测试,但它们缺少与集成测试有关的功能。例如,它们直接调用请求处理方法,无法测试请求映射和数据绑定,它们也不测试bean依赖注入,因为被测试Controller是用new进行实例化。
所以需要使用新的测试类型和API。MockMvc类位于org.springframework.test.web.servlet包下,是Spring中用于帮助集成测试的。此类允许你使用预定的请求映射来调用请求处理方法。创建MockMvc实例的方法:
MockMvc mockMvc=MockMvcBuidlers.webAppContextSetip(webAppContext).build();
这里的webAppContext是WebApplicationContext实例的一个引用,WebApplicationContext是ApplicationContext的一个常用子类。要获得一个WebApplicationContext,你必须在测试类中声明这一点:
@Autowried private WebApplicationContext webAppContext;
MockMvc其实是一个很简单的类,事实上,它只有一个方法:perform,用于通过URI间接调用SpringMVC控制器。preform方法签名如下:
ResultActions perform(ResultBuilder requestBuilder)
要测试请求处理方法,你需要创建一个RequestBuilder。好在可以使用MockMvcRequestBuilders类提供的与HTTP方法具有相同名称的静态方法:get。post,head,put,patch,delete等。要使用HTTP GET方法测试控制器,你可以调用get方法,测试POST调用post方法即可。例如:
ResultActions resultActions=mockMvc。perform(get("/请求URI"));
要验证是否成功,需要调用ResultActions的andExpect方法。andExpect方法签名如下:
ResultAction andExpect(ResultMatcher matcher)
注意,andExpect方法返回ResultActions的另一个实例,这意味着可以链式调用多个AndExpect方法。该方法的参数为ResultMatcher,MockMvcResultMatchers类提供了静态的方法来轻松创建ResultMatcher.MockMvcResultMatchers属于org.springframework.test.web.servlet.result包。它的部分静态方法如下:
方法 | 返回值 | 描述 |
cookie | CookieResultMatchers | 返回一个ResultMatchers,用于断言cookie值 |
header | HeaderResultMatchers | 返回一个ResultMatchers,用于断言HTTP响应头 |
model | ModelResultMatchers | 返回一个ResultMatchers,用于断言请求处理的模型 |
status | StatusResultMatchers | 返回一个ResultMatchers,用于断言HTTP响应状态 |
view | ViewResultMatchers | 返回一个ResultMatchers,用于断言请求处理的视图 |
例如,要保证控制器方法的请求映射正确,可以使用状态方法:
mockMvc.preform(get('/请求URI').andExpect(status().isOk()));
isOk方法返回状态码200.
例子:
请求处理方法
package cn.lynu.controller.test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import cn.lynu.controller.test.domain.Employee; import cn.lynu.controller.test.service.EmployeeService; @Controller public class EmployeeController { @Autowired private EmployeeService employeeService; @RequestMapping("/highest-paid/{category}") public String getHighestPaidEmployee(@PathVariable int category,Model model) { Employee employee = employeeService.getHighestPaidEmployee(category); model.addAttribute("employee", employee); return "success"; } }
测试方法
package cn.lynu.controller.test; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; /** * 集成测试 * @author lz * */ @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration("/webapp") @ContextConfiguration("classpath:test-config.xml") public class EmployeeControllerTest { @Autowired private WebApplicationContext webApplicationContext; private MockMvc mockMvc; @Before public void before() { this.mockMvc=MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); } @Test public void testGetHighestPaidEmployee() { try { mockMvc.perform(get("/highest-paid/1")) .andExpect(status().isOk()) .andExpect(model().attributeExists("employee")) .andDo(print()); } catch (Exception e) { e.printStackTrace(); } } }
这里的测试开始与单元测试有所不同。首先是测试类运行器。需要一个SpringJUnit4ClassRunner.class在@RunWith注解内:
@RunWith(SpringJUnit4ClassRunner.class)
这个runner允许你使用spring。
然后添加如下注解类型:
@WebAppConfiguration("/webapp")
@ContextConfiguration("classpath:test-config.xml")
WebAppConfiguration注解类型用于声明为集成测试加载的ApplicationContext应该为WebApplicationContext,并指定webapp为项目的根目录,这是一个Maven项目的根目录,如果不是Maven项目就修改为对应项目的根目录,不然可能会出问题。
ContextConfiguration注解告诉测试运行器加载哪个配置文件,这个test-config.xml就是一个基本的springMVC配置文件。
除此之外,测试类还需要这两个对象:
@Autowired private WebApplicationContext webApplicationContext; private MockMvc mockMvc;
webApplicationContext 会在@before修饰的测试方法中被初始化:
@Before public void before() { this.mockMvc=MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); }
在测试方法中,我们期望Controller方法响应状态码为200,并且模型具有employee属性。andDo(print())方法的调用将打印各种值。
其他常用注解
@RequestHeader
请求头包含了若干个属性,服务器可据此获知客户端的信 息,通过 @RequestHeader 即可将请求头中的属性值绑定到处理方法的入参中
这里可以通过给@RequestHeader注解的value指定值来获得所需的请求头信息,也可以不指定,不指定的情况下就去获取参数名对应的请求头信息:
@CookieValue
使用@CookieValue 可让处理方法入参绑定某个 Cookie 值
/** * 使用@CookieValue */ @RequestMapping("/testCookieValue") public void testCookieValue(@CookieValue(value="sessionId",required=false) String sessionId) { System.out.println(sessionId); }
@SessionAttributes
这个注解是需要放在Controller类上方面的,也只能放在类上。作用是将使用@ModelAttribute修饰的数据放到session中,这样我们放在Model中的数据(根据数据名或数据类型)就会添加进session:
这样名为name2的,被@ModelAttribute修饰的对象就会被放入session中,但是这个注解存在一些问题:
springMVC 先在Model中寻找对应key的值,如果没有找到再看handler有没有@SessionAttributes,如果有这个注解,如果有就从session中寻找,没找到就会抛这个异常;没有这个注解才创建一个新的POJO也没有异常。也就说该注解可能会触发一个异常
所以,如果需要将数据放在session中,我们还是使用HttpSession作为请求处理方法的形参这种方式吧。HttpSession是可以作为SpringMVC方法的形参的。
mvc-view-controller 配置
我们都知道将页面放到WEB-INF下,就不能直接访问到,只能通过程序访问到,这里面的页面相互之间也是访问不到的(通过超链接),所以如果希望还可以直接访问WEB-INF下的页面,需要在SpringMVC配置文件中配置 mvc-view-controller ,这样访问就不走控制器了,但是需要注意的是该配置需要 annotation-driver 配置的支持(这一点同配置 default-servlet-handler 一样),否则,控制器里的所有请求处理方法都失效: