但行好事
莫论前程❤

MockMvc测试Spring mvc Controller

概述

​ 对模块进行集成测试时,希望能够通过输入URL对Controller进行测试,如果通过启动服务器,建立http client进行测试,这样会使得测试变得很麻烦,比如,启动速度慢,测试验证不方便,依赖网络环境等,这样会导致测试无法进行,为了可以对Controller进行测试,可以通过引入MockMVC进行解决。

简介

MockMvc实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,这样可以使得测试速度快、不依赖网络环境,而且提供了一套验证的工具,这样可以使得请求的验证统一而且很方便。

要求

spring 集成测试中对mock 的集成很好,让开发员使用起来很方便,但是使用时必须注意以下三个条件:

  1. Junit必须在4.9版本以上
  2. spring版本必须在3.2以上
  3. 使用的框架必须是springMvc框架

现状

目前的测试流程图:

img

1.直接使用httpClient 这方法各种麻烦

2.使用Spring 提供的RestTemplate错误不好跟踪,必须开着服务器

spring开发中,可以使用Spring自带的MockMvc这个类进行Mock测试

所谓的Mock测试,这里我举一个通俗易懂的例子,像servlet API中的HttpServletRequest对象是Tomcat容器生成的。我们无法手动的new出来,于是就有了所谓的Mock测试

运行配置

用到的注解:

  • @RunWith(SpringJUnit4ClassRunner.class): 使用Spring-Test框架;在使用所有注释前必须使用 @RunWith(SpringJUnit4ClassRunner.class),让测试运行于Spring测试环境。

  • @WebAppConfiguration: 使用这个Annotate会在跑单元测试的时候真实的启动一个web服务,然后开始调用Controller的Rest API,待单元测试跑完之后再将web服务停掉;

  • @ContextConfiguration: 指定需要加载的spring配置文件的地址 ,可以有多种方式,这个例子使用的是文件路径形式,如果有多个配置文件,可以将括号中的信息配置为一个字符串数组来表示;controller,component等都是使用注解,需要注解指定spring的配置文件,扫描相应的配置,将类初始化等。

  • @TransactionConfiguration(transactionManager="transactionManager",defaultRollback=true)配置事务的回滚,对数据库的增删改都会回滚,便于测试用例的循环利用
  • @Mock: 需要被Mock的对象
  • @Transactional:不是必须的,是和@TestExecutionListeners中的TransactionalTestExecutionListener.class配合使用,用于保证插入的数据库中的测试数据,在测试完后,事务回滚,将插入的数据给删除掉,保证数据库的干净。如果没有显示的指定@Transactional,那么插入到数据库中的数据就是真实数据。
  • @InjectMocks: 需要将Mock对象注入的对象, 此处就是Controller
  • @Before: 在每次Test方法之前运行的方法,目前把登陆信息放到session中处理,以及初始化mockMvc。
    • mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
  • @Test:执行测试

为什么要进行事务回滚:

  • 测试过程对数据库的操作,会产生脏数据,影响我们数据的正确性
  • 不方便循环测试,即假如这次我们将一个记录删除了,下次就无法再进行这个Junit测试了,因为该记录已经删除,将会报错。
  • 如果不使用事务回滚,我们需要在代码中显式的对我们的增删改数据库操作进行恢复,将多很多和测试无关的代码

实际运用

父类:

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.transaction.TransactionConfiguration;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;

/** 
 * @author zl 
 * @version 创建时间:2017年3月14日 下午2:18:26 
 * 
 */
//这个必须使用junit4.9以上才有
@RunWith(SpringJUnit4ClassRunner.class)
//单元测试的时候真实的开启一个web服务
@WebAppConfiguration
//配置事务的回滚,对数据库的增删改都会回滚,便于测试用例的循环利用
@TransactionConfiguration(transactionManager="transactionManager",defaultRollback=true)
@Transactional
@ContextConfiguration(locations = {"classpath:spring.xml","classpath:spring-hibernate.xml"})
public class AbstractContextControllerTests {

    @Autowired
    protected WebApplicationContext wac;
}

子类:

package com.pengtu.gsj;

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.status;

import org.hibernate.SessionFactory;
import org.junit.Before;
import org.junit.Test;
import org.owasp.esapi.ESAPI;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import com.pengtu.gsj.controller.BannerController;
import com.pengtu.gsj.dao.UserDao;
import com.pengtu.gsj.entity.app.User;
import com.pengtu.gsj.service.UserService;

public class EsapiTest extends AbstractContextControllerTests{

    private MockMvc mockMvc;
    //该方法在每个方法执行之前都会执行一遍
    @Before
    public void setUp() throws Exception {
        //初始化MockMvc对象
        mockMvc = MockMvcBuilders.standaloneSetup(new BannerController()).build();
    }

    /**
     * perform:执行一个RequestBuilder请求,会自动执行SpringMVC的流程并映射到相应的控制器执行处理;
     * get:声明发送一个get请求的方法。
       MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables):
       根据uri模板和uri变量值得到一个GET请求方式的。另外提供了其他的请求的方法,如:post、put、delete等。
     * param:添加request的参数,如上面发送请求的时候带上了了pcode = root的参数。假如使用需要发送json数据格式的时将不能使用这种方式,可见后面被@ResponseBody注解参数的解决方法
     * andExpect:添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确(对返回的数据进行的判断);
     * andDo:添加ResultHandler结果处理器,比如调试时打印结果到控制台(对返回的数据进行的判断);
     * andReturn:最后返回相应的MvcResult;然后进行自定义验证/进行下一步的异步处理(对返回的数据进行的判断)
     * @throws Exception
     */
    @Test
    public void getAllBanners() throws Exception{
         String responseString = mockMvc.perform(get("/banner/hello") //请求的url,请求的方法是get
                            .contentType(MediaType.APPLICATION_JSON)  //数据的格式
                            .param("id","123456789")         //添加参数
            ).andExpect(status().isOk())    //返回的状态是200
                    .andDo(print())         //打印出请求和相应的内容
                    .andReturn().getResponse().getContentAsString();   //将相应的数据转换为字符串
            System.out.println("--------返回的json = " + responseString);
    }
}

对应controller的方法:

 @RequestMapping("/hello")
     @ResponseBody
     public String index(String id) {
         System.out.println("id:"+id);
            return "Hello World";
     }

执行测试类后的结果:

MockHttpServletRequest:
         HTTP Method = GET
         Request URI = /banner/hello
          Parameters = {id=[123456789]}
             Headers = {Content-Type=[application/json]}

             Handler:
                Type = com.pengtu.gsj.controller.BannerController
              Method = public java.lang.String com.pengtu.gsj.controller.BannerController.index(java.lang.String)

  Resolved Exception:
                Type = null

        ModelAndView:
           View name = null
                View = null
               Model = null

            FlashMap:

MockHttpServletResponse:
              Status = 200
       Error message = null
             Headers = {Content-Type=[text/plain;charset=ISO-8859-1], Content-Length=[11]}
        Content type = text/plain;charset=ISO-8859-1
                Body = Hello World
       Forwarded URL = null
      Redirected URL = null
             Cookies = []
--------返回的json = Hello World

具体测试的方法

测试方法中的mockMvc需要调用的方法:

  • mockMvc.perform执行一个请求(该请求由2构造);
  • MockMvcRequestBuilders.get(“/user/1”)构造一个请求
  • ResultActions.andExpect添加执行完成后的断言,目的测试返回结果,通过返回结果来确认测试是否成功
  • ResultActions.andDo添加一个结果处理器,表示要对结果做点什么事情,比如此处使用MockMvcResultHandlers.print()输出整个响应结果信息。
  • ResultActions.andReturn表示执行完成后返回相应的结果。

测试逻辑:

Mock出一个MockHttpServletRequestBuilder对象。用于模拟Http的get请求方式,

.param()方法可以给http请求携带参数,相当于 写了这样一个url ——> http:localhost:8080/banner/hello?id=123456789

然后调用.andExport( status().isOk())方法看请求的状态响应码是否为200如果不是则抛异常,测试不通过

遇到的问题:

发送一个被@ResponseBody标识的参数,一直到400错误。 即无法发送一个json格式的数据到Controller层。
解决方法:

SoftInfo softInfo = new SoftInfo();
//。。。设置值
    String requestJson = JSONObject.toJSONString(folderInfo);
        String responseString = mockMvc.perform(post("/softs")
                                     .contentType(MediaType.APPLICATION_JSON)
                                     .content(requestJson)).andDo(print())
                                    .andExpect(status().isOk()).andReturn()
                                    .getResponse().getContentAsString(); 

注意上面contentType需要设置成MediaType.APPLICATION_JSON,即声明是发送“application/json”格式的数据。使用content方法,将转换的json数据放到request的body中。

后台的返回数据中,最好带上我们对数据库的修改的结果返回的前端。

为什么要在data中返回一个修改或者添加的对象

  • 将数据返回给前端,前端容易判断数据是否添加或者修改成功
  • 更新或者添加完数据经常需要刷新页面,将数据直接给了前端,前端不用再发一个请求来获取
  • 单元测试的时候,能对数据库的DDL(增删改)操作的时候,我们能对数据进行审核,从何判断我们的操作是否是成功的。如下面的例子:

我们发送一个添加操作,添加一个SoftInfo对象,SoftInfo类定义如下:

public class SoftInfo {
    private String id;
    private String name;
}

添加完之后,由于我们进行了单元测试的事务回滚,我们将不能再数据库中看我们我们的的添加操作,无法判断操作是否成功

为了解决上面的问题,我们可以在返回的json的数据中添加一个“data”字段,解析该json中的data字段数据,判断我们的添加操作是否成功的。json格式如下:

{
    "status":200,
    "data":{"id":"2","name":"测试"}
}

我们可以使用andExpect方法对返回的数据进行判断,用“\$.属性”获取里面的数据,

如我要获取返回数据中的”data.name”,可以写成”$.data.name”。

下面的例子是判断返回的data.name=“测试”。

@Test
    public void testCreateSeewoAccountUser() throws Exception {
        mockMvc.perform(post("/users")
                        .contentType(MediaType.APPLICATION_FORM_URLENCODED)  
        ).andExpect(status().isOk())
        .andExpect(jsonPath("$.data.name", is("测试"))))  
        .andExpect(jsonPath("$.data.createTime", notNullValue()))
        ;
    }

拓展

其中DefaultMockMvcBuilder还提供了如下API:

  • addFilters(Filter… filters)/addFilter(Filter filter, String… urlPatterns):添加javax.servlet.Filter过滤器
  • defaultRequest(RequestBuilder requestBuilder):默认的RequestBuilder,每次执行时会合并到自定义的RequestBuilder中,即提供公共请求数据的;
  • alwaysExpect(ResultMatcher resultMatcher):定义全局的结果验证器,即每次执行请求时都进行验证的规则;
  • alwaysDo(ResultHandler resultHandler):定义全局结果处理器,即每次请求时都进行结果处理;
  • dispatchOptions:DispatcherServlet是否分发OPTIONS请求方法到控制器;

StandaloneMockMvcBuilder继承了DefaultMockMvcBuilder,又提供了如下API:

  • setMessageConverters(HttpMessageConverter
  • setValidator(Validator validator):设置验证器;
  • setConversionService(FormattingConversionService conversionService):设置转换服务;
  • addInterceptors(HandlerInterceptor… interceptors)/addMappedInterceptors(String[] pathPatterns, HandlerInterceptor… interceptors):添加spring mvc拦截器;
  • setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager):设置内容协商管理器;
  • setAsyncRequestTimeout(long timeout):设置异步超时时间;
  • setCustomArgumentResolvers(HandlerMethodArgumentResolver… argumentResolvers):设置自定义控制器方法参数解析器;
  • setCustomReturnValueHandlers(HandlerMethodReturnValueHandler… handlers):设置自定义控制器方法返回值处理器;
  • setHandlerExceptionResolvers(List
  • setViewResolvers(ViewResolver…resolvers):设置视图解析器;
  • setSingleView(View view):设置单个视图,即视图解析时总是解析到这一个(仅适用于只有一个视图的情况);
  • setLocaleResolver(LocaleResolver localeResolver):设置Local解析器;
  • setFlashMapManager(FlashMapManager flashMapManager):设置FlashMapManager,如存储重定向数据;
  • setUseSuffixPatternMatch(boolean useSuffixPatternMatch):设置是否是后缀模式匹配,如“/user”是否匹配”/user.*”,默认真即匹配;
  • setUseTrailingSlashPatternMatch(boolean useTrailingSlashPatternMatch):设置是否自动后缀路径模式匹配,如“/user”是否匹配“/user/”,默认真即匹配;
  • addPlaceHolderValue(String name, String value) :添加request mapping中的占位符替代;
  • StandaloneMockMvcBuilder不会加载Spring MVC配置文件,因此就不会注册我们需要的一些组件,因此就提供了如上API用于注册我们需要的相应组件。
  • perform:执行一个RequestBuilder请求,会自动执行SpringMVC的流程并映射到相应的控制器执行处理;
  • andExpect:添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确;
  • andDo:添加ResultHandler结果处理器,比如调试时打印结果到控制台;
  • andReturn:最后返回相应的MvcResult;然后进行自定义验证/进行下一步的异步处理;

MockMvcRequestBuilders主要API:

  • MockHttpServletRequestBuilder get(String urlTemplate, Object… urlVariables):根据uri模板和uri变量值得到一个GET请求方式的MockHttpServletRequestBuilder;如get(“/user/{id}”, 1L);
  • MockHttpServletRequestBuilder post(String urlTemplate, Object… urlVariables):同get类似,但是是POST方法;
  • MockHttpServletRequestBuilder put(String urlTemplate, Object… urlVariables):同get类似,但是是PUT方法;
  • MockHttpServletRequestBuilder delete(String urlTemplate, Object… urlVariables) :同get类似,但是是DELETE方法;
  • MockHttpServletRequestBuilder options(String urlTemplate, Object… urlVariables):同get类似,但是是OPTIONS方法;
赞(0) 打赏
未经允许不得转载:刘鹏博客 » MockMvc测试Spring mvc Controller
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!

 

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏