Spring MVC Test Framework简译

来源:互联网 发布:好看的动漫 知乎 编辑:程序博客网 时间:2024/06/16 15:49

10.3.6 Spring MVC Test Framework

Spring MVC 测试框架(Spring MVC Test framework)支持junit的,用于测试客户端/(Spring MVC编码)服务器端的API。它通过TestContext框架加载spring配置文件,DispatcherServlet处理请求。它基本上接近于全量集成测试,但是不需要启动Servlet容器。

客户端测试需要基于RestTemplate,并依赖于RestTemplate编写测试用例,不需要启动一个server来响应请求。

Spring MVC测试框架原理是在DispatcherServlet调用controller时,重写controller,用于执行请求和生成响应。此时仍可以mock Controller 依赖的对象,并注入到Controller中。因此测试仍可以只关注web层。

Spring MVC测试基于Servlet API的mock实现,这样处理请求和生成返回信息就不用启动Servlet容器了。除了渲染JSP 页面外,其它功能都可以在Spring MVC框架中被测试。如果你知道MockHttpServletResponse是如何工作的,你就会知道forwards和redirects并没有指正执行,而是url地址被保存下来,然后可以在测试代码中验证。换句话说,如果你使用JSP文件,你可以验证一个请求的forward到哪个jsp页面。

也可以说,所有包含 @ResponseBody 和返回View类型(Freemarker, Velocity, Thymeleaf,jsp)的用于产生HTML、JSON、XML内容的方法,都可以在Spring MVC测试框架下如预期一样工作,并在response中包含生成的内容。

下面的测试用例需要一个JSON格式的账户信息:

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;@RunWith(SpringJUnit4ClassRunner.class)@WebAppConfiguration@ContextConfiguration("test-servlet-context.xml")public class ExampleTests {    @Autowired    private WebApplicationContext wac;    private MockMvc mockMvc;    @Before    public void setup() {        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();    }    @Test    public void getAccount() throws Exception {        this.mockMvc.perform(get("/accounts/1")          .accept(MediaType.parseMediaType("application/json;charset=UTF-8")))            .andExpect(status().isOk())            .andExpect(content().contentType("application/json"))            .andExpect(jsonPath("$.name").value("Lee"));    }}

这个测试用例依赖TestContext框架提供的WebApplicationContext类。该用例通过xml文件加载Spring配置,并将WebApplicationContext实例注入到test中,并用WebApplicationContext创建了MockMvc实例。

然后使用MockMvc执行了一个“/accounts/1”的请求,并验证response的状态是否是200,内容类型是否是”application/json”,并且返回的JSON数据中是否包含“name”属性,且其值是“Lee”。Json数据的解析参见Jayway的JsonPath project。还有很多类似的用于验证返回值的方法,这些方法会在后面讨论。

Static Imports

上面的列子中使用静态导入,比如:MockMvcRequestBuilders.*, MockMvcResultMatchers.*, and MockMvcBuilders.*. 一个简单的找到这些类的方法是搜索以“MockMvc”开头的类。如果使用Eclipse,你可以将这些类加入到“favorite static members”中:Java → Editor → Content Assist → Favorites。这样就可以在输入静态方法的首字符时,Eclipse会弹出代码补全提示。其它IDE(比如IntelliJ)可能不需要任何配置,是否需要配置需要你检查你的IDE是否支持静态对象的代码补全功能。

Setup Options

服务端测试setup的目的是创建MockMvc实例,以用于执行请求。有两种方式用来创建MockMvc实例。

第一中方式是加载spring配置,并在测试代码中注入WebApplicationContext类,然后用WebApplicationContext创建一个MockMvc对象:

@RunWith(SpringJUnit4ClassRunner.class)@WebAppConfiguration@ContextConfiguration("my-servlet-context.xml")public class MyWebTests {    @Autowired    private WebApplicationContext wac;    private MockMvc mockMvc;    @Before    public void setup() {        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();    }    // ...}

第二个选项是不加载spring配置,只简单的注册一个controller实例。

public class MyWebTests {    private MockMvc mockMvc;    @Before    public void setup() {        this.mockMvc = MockMvcBuilders.standaloneSetup(new AccountController()).build();    }    // ...}

你将会用那种方式呢?

使用“webAppContextSetup”方式会加载spring mvc的配置文件,这样会有一个更加齐全的集成测试环境。因为TestContext框架缓存了spring配置,所以可以多个测试方法执行的更快。此外,你还可以通过spring配置文件mock一些service类,这样就可以让测试只关注web层。下面是通过Mockitomock了一个service类:

<bean id="accountService" class="org.mockito.Mockito" factory-method="mock">    <constructor-arg value="org.example.AccountService"/></bean>

然后你就可以将mock service注入到test类中,然后验证期望:

@RunWith(SpringJUnit4ClassRunner.class)@WebAppConfiguration@ContextConfiguration("test-servlet-context.xml")public class AccountTests {    @Autowired    private WebApplicationContext wac;    private MockMvc mockMvc;    @Autowired    private AccountService accountService;    // ...}

“standaloneSetup”的方式,从某方面来说更接近单元测试。它一次只测试一个controller,这个controller可以手工注入mock依赖,并不需要加载spring配置文件。这样的测试更容易看出在测试哪个controller,是否需要加载特殊的Spring MVC配置,等等。这种方式也更容易写ad-hoc测试来验证某些行为或测试一个问题。

正像集成测试vs单元测试,并没有谁对谁错的答案。使用“standaloneSetup”方式时需要额外使用“webAppContextSetup”方式来验证Spring MVC的配置。当然,你可以所有的测试用例都用“webAppContextSetup”的方式来写。

Performing Requests

为了执行一个请求,你需要指定使用合适的HTTP method,并指定MockHttpServletRequest请求内容的格式:

比如:

mockMvc.perform(post("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON));

另外,对所有的http method,你都可以执行一个上传文件请求(它会在内部创建一个MockMultipartHttpServletRequest实例):

mockMvc.perform(fileUpload("/doc").file("a1", "ABC".getBytes("UTF-8")));

查询参数可以使用URI模块定义:

mockMvc.perform(get("/hotels?foo={foo}", "bar"));

或者增加增加请求参数:

mockMvc.perform(get("/hotels").param("foo", "bar"));

如果应用程序代码需要请求时携带参数,多数情况下,并不会检查请求参数字符串,此时可以随意增加参数。请记住如果使用URI模板携带请求参数则参数需要encode,使用param(…)方法参数不需要encode。

大部分情况下并不需要使用context path 和 Servlet path,但是如果你需要测试请求的全路径,你需要设置contextPath和servletPath:

mockMvc.perform(get("/app/main/hotels/{id}").contextPath("/app").servletPath("/main"))

看上面的例子,为每一个request设置contextPath和servletPath设置值是很笨重的,你可以在构建MockMvc对象时设置默认的request属性:

public class MyWebTests {    private MockMvc mockMvc;    @Before    public void setup() {        mockMvc = standaloneSetup(new AccountController())            .defaultRequest(get("/")            .contextPath("/app").servletPath("/main")            .accept(MediaType.APPLICATION_JSON).build();    }

上面的request属性会应用在所有MockMvc的请求中。如果在某个请求中设置了某属性,则会覆盖MockMvc中相应属性的默认值。这也是为什么可以设置HTTP 方法 和 URI 的原因,因为每一个request都必须设置这两个值。

Defining Expectations

期望:你可以在perform之后使用.andExpect(..) 定义一个或多个期望。

mockMvc.perform(get("/accounts/1")).andExpect(status().isOk());

在MockMvcResultMatchers.* 中定义了大量静态的方法,有些方法的返回类型可用于断言请求的结果。断言一般情况下分为两类。

一类断言用来验证response的属性,比如status,headers,content。这些正是测试最重要的内容。

第二类断言用来检查Spring MVC 具体结构,比如哪个controller方法处理的request,是否有异常抛出并被处理,model的内容是什么,选择了哪个view,增加了哪些临时属性,等等。也可以验证Servlet的具体结构,比如request和session的属性 。下面是测试 绑定/验证 失败的断言:

mockMvc.perform(post("/persons"))    .andExpect(status().isOk())    .andExpect(model().attributeHasErrors("person"));

大部分情况下,dump请求的结果是很有用的。可以像下面这样使用MockMvcResultHandlers 的print():

mockMvc.perform(post("/persons"))    .andDo(print())    .andExpect(status().isOk())    .andExpect(model().attributeHasErrors("person"));

一旦请求产生了未捕获的异常,print()方法会向System.out 中打印所有可能的结果。

在某些情况下,你可能想直接获取请求结果,尤其在使用期望无法验证的时候,你可以在所有的期望后增加.andReturn()方法:

MvcResult mvcResult = mockMvc.perform(post("/persons"))        .andExpect(status().isOk()).andReturn();// ...

当所有的测试都有相同的期望时,你可以在构建MockMvc时定义通用的期望:

standaloneSetup(new SimpleController())    .alwaysExpect(status().isOk())    .alwaysExpect(content().contentType("application/json;charset=UTF-8"))    .build()

注意:期望每次都会被应用并且不能被覆盖,除非使用不同的MockMvc实例。

当响应的JSON文本中包含了Spring HATEOAS形式的 超媒体链接时,这个链接可以这样验证:

mockMvc.perform(get("/people").accept(MediaType.APPLICATION_JSON))    .andExpect(jsonPath("$.links[?(@.rel == self)].href")    .value("http://localhost:8080/people"));

当响应的XML文本中包含了Spring HATEOAS形式的 超媒体链接时,这个链接可以这样验证:

Map<String, String> ns = Collections.singletonMap("ns", "http://www.w3.org/2005/Atom");mockMvc.perform(get("/handle").accept(MediaType.APPLICATION_XML))    .andExpect(xpath("/person/ns:link[@rel=self]/@href", ns)    .string("http://localhost:8080/people"));

Filter Registrations

在生成MockMvc对象时,你可以注册一个或多个Filter:

mockMvc = standaloneSetup(new PersonController())      .addFilters(new CharacterEncodingFilter()).build();

注册的过滤器会被spring-test框架的MockFilterChain调用,最后一个filter会委托给DispatcherServlet.

Further Server-Side Test Examples

该框架自己的测试用例中包含很多简单的测试用例,用来说明怎样使用Spring MVC Test。也可以查看spring-mvc-showcase项目,这里有使用Spring MVC Test的完整的覆盖测试。

Client-Side REST Tests

客户端测试需要使用RestTemplate:其定义了对request的期望并提供了一个“预存”的响应:

RestTemplate restTemplate = new RestTemplate();MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate);mockServer.expect(requestTo("/greeting")).andRespond(withSuccess("Hello world", "text/plain"));// use RestTemplate ...mockServer.verify();

在上例中,MockRestServiceServer是通过ClientHttpRequestFactory配置RestTemplate,通过期望断言请求,并返回“预存”的响应。在这个例子中,我们期望一个”/greeting”请求,并获得一个状态200的”text/plain”类型的响应。我们根据需要定义任意多个请求和预存的响应。

一旦定义了requests和responses,RestTemplate就可以在客户端测试代码中使用了。测试例子中的最后
一行代码“mockServer.verify()”,能验证是否执行了期望的requests。

Static Imports

和服务端一样,客户单测试可需要一些静态导入。可以搜索”MockRest*”找打这些需要静态导入的类。Eclipse使用者可以将”MockRestRequestMatchers.” and “MockRestResponseCreators.“增加到“favorite static members”中:Java → Editor → Content Assist → Favorites。这样就可以在输入静态方法的首字符时,Eclipse会弹出代码补全提示。其它IDE(比如IntelliJ)可能不需要任何配置,是否需要配置需要你检查你的IDE是否支持静态对象的代码补全功能。

Further Examples of Client-side REST Tests

Spring MVC测试框架里面的测试用例,包含了对客户端REST测试的例子。

附加两个具体的测试用例

例子1,加载spring 配置文件的集成测试:

/** * 集成式的测试 * * @author jeff * */@RunWith(SpringJUnit4ClassRunner.class)@WebAppConfiguration(value = "src/main/webapp")@ContextConfiguration(locations = { "classpath:/spring/spring-test-web.xml" })public class OperationDriverControllerTest  {    @Autowired    protected WebApplicationContext wac;    protected MockMvc mockMvc;    @Before    public void setUp() {        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();    }    /**     * 区长管理首页测试     *     * @throws Exception     */    @Test    public void platoonManageDriverTest() throws Exception {        MvcResult result = mockMvc.perform(MockMvcRequestBuilders    .post("/operationDriver/platoonManageDriverPage"))                .andExpect(MockMvcResultMatchers.view()        .name("platoonManageDriver/index"))        .andDo(MockMvcResultHandlers.print()).andReturn();        Assert.assertNotNull(result.getModelAndView().getViewName());    }    /**     * 区长管理工具查询接口测试     *     * @throws Exception     */    @Test    public void exportPlatoonManageDriverTest() throws Exception {        long dt = 1507651200L;        Integer cityId = 1;        int pageNo = 1;        int pageSize = 10;        MvcResult result = mockMvc                .perform(MockMvcRequestBuilders.post(                        "/operationDriver/platoonManageDriver?dt=${dt}            &cityId=${cityId}            &pageNo=${pageNo}&pageSize=${pageSize}",            dt, cityId, pageNo, pageSize))                .andExpect(MockMvcResultMatchers.status().isOk())        .andDo(MockMvcResultHandlers.print()).andReturn();        Assert.assertNotNull(result.getResponse().getContentAsString());    }}

例子2,standaloneSetup+mockito 测试单个controller:

/** * 单个controller mock测试 * * @author jeff * */public class OperationDriverControllerTest2 {    private MockMvc mockMvc;    //要测试的controller    private OperationDriverController operationDriverController    = new OperationDriverController();    /**     * controller的依赖,此处通过接口mock     */    @Mock    private DriverOperationService driverOperationService;    private long dt = 1507651200L;    private Integer cityId = 1;    private int pageNo = 1;    private int pageSize = 10;    private Integer warehouseId = null;    private Long stationRegionId = null;    private String driverName = null;    private Long driverAuthId = null;    private String squadName = null;    private String platoonName = null;    Page<PlatoonManageDriverBean> platoonManageDriverQuery    = new Page<PlatoonManageDriverBean>(pageSize, pageNo);    private ServiceResult<Page<PlatoonManageDriverBean>> queryResult    = ServiceResultUtil.success(platoonManageDriverQuery);    @Before    public void setup() {        MockitoAnnotations.initMocks(this);        /**         * stub mock对象的response         */        Mockito.when(driverOperationService.platoonManageDriver    (DateUtil.formatYMD(DateUtil.fromUnixtime(dt)), cityId, warehouseId,                stationRegionId, driverName, driverAuthId, squadName,         platoonName, pageNo, pageSize)).thenReturn(queryResult);        //向controller中注入mock对象        ReflectionTestUtils.setField(operationDriverController,     "driverOperationService", driverOperationService);        this.mockMvc =MockMvcBuilders.standaloneSetup(operationDriverController)        .build();    }    /**     * 区长管理首页测试     *     * @throws Exception     */    @Test    public void platoonManageDriverTest() throws Exception {        MvcResult result = mockMvc.perform(MockMvcRequestBuilders    .post("/operationDriver/platoonManageDriverPage"))                .andExpect(MockMvcResultMatchers.view()        .name("platoonManageDriver/index"))        .andDo(MockMvcResultHandlers.print()).andReturn();        Assert.assertNotNull(result.getModelAndView().getViewName());    }    /**     * 区长管理工具查询接口测试     *     * @throws Exception     */    @Test    public void exportPlatoonManageDriverTest() throws Exception {        MvcResult result = mockMvc                .perform(MockMvcRequestBuilders.        post(                        "/operationDriver/platoonManageDriver?dt=${dt}            &cityId=${cityId}&pageNo=${pageNo}&pageSize=${pageSize}",             dt, cityId,                        pageNo, pageSize))                .andExpect(MockMvcResultMatchers.status().isOk())        .andDo(MockMvcResultHandlers.print()).andReturn();        Assert.assertNotNull(result.getResponse().getContentAsString());    }}