其他

测试

有两个很重要的测试时单元测试和集成测试,通常是从单元测试开始测试类中的单个方法,然后进行集成测试,以测试不同的模块是否可以无缝协同工作。

单元测试

我们应该对控制器中的每个方法进行单元测试。但是测试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  一样),否则,控制器里的所有请求处理方法都失效:

 

posted @ 2018-01-31 15:33  OverZeal  阅读(509)  评论(0编辑  收藏  举报