Spring MVC Test Framework简译

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());
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值