收藏私塾在线
 

欢迎您来到私塾在线网!   

请登录! 

免费注册 


zhang的笔记
状态: 离线
人气:5090722
访问用户量:4227
笔记经验:
总积分:261656
级别:VIP5
搜索本笔记
ta的交流分类
ta的交流主题贴(544)
ta的所有交流贴(1049)
ta的全部笔记
全部笔记(255)
未分类笔记(1)
Java Web(9)
并发实践(1)
课程问题(0)
Java(22)
架构(1)
缓存(5)
JavaEE(0)
JVM(12)
跟我学spring3(68)
Spring Sec……(43)
Spring 3.x……(25)
Spring Sec……(20)
跟开涛学Spring……(17)
深入剖析Spring……(18)
性能调优(10)
前端(2)
Tomcat源码解读(1)
spring sec……(0)
存档
2014-01(7)
2013-12(10)
2012-10(4)
2012-09(2)
2012-08(31)
2012-07(10)
2012-06(5)
2012-05(41)
2012-04(3)
2012-03(41)
2012-02(54)
2011-11(17)
2011-10(30)

2014-01-20 08:26:28
Spring MVC测试框架详解——客户端测试
浏览(23410)|评论(0)   交流分类:Java|笔记分类: 跟我学spring3

上一篇《Spring MVC测试框架详解——服务端测试》已经介绍了服务端测试,接下来再看看如果测试Rest客户端,对于客户端测试以前经常使用的方法是启动一个内嵌的jetty/tomcat容器,然后发送真实的请求到相应的控制器;这种方式的缺点就是速度慢;自Spring 3.2开始提供了对RestTemplate的模拟服务器测试方式,也就是说使用RestTemplate测试时无须启动服务器,而是模拟一个服务器进行测试,这样的话速度是非常快的。

 

2 RestTemplate客户端测试

整个环境在上一篇《Spring MVC测试框架详解——服务端测试》基础上进行构建。

 

UserRestController控制器

@RestController
@RequestMapping("/users")
public class UserRestController {

    private UserService userService;

    @Autowired
    public UserRestController(UserService userService) {
        this.userService = userService;
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    public User findById(@PathVariable("id") Long id) {
        return userService.findById(1L);
    }

    @RequestMapping(method = RequestMethod.POST)
    public ResponseEntity<User> save(@RequestBody User user, UriComponentsBuilder uriComponentsBuilder) {
        //save user
        user.setId(1L);
        MultiValueMap headers = new HttpHeaders();
        headers.set("Location", uriComponentsBuilder.path("/users/{id}").buildAndExpand(user.getId()).toUriString());
        return new ResponseEntity(user, headers, HttpStatus.CREATED);
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE)
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void update(@RequestBody User user) {
        //update by id
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
    public void delete(@PathVariable("id") Long id) {
        //delete by id
    }
}

 

UserService

package com.sishuok.mvc.service;

import com.sishuok.mvc.entity.User;

public interface UserService {
    public User findById(Long id);
}
 

UserServiceImpl

package com.sishuok.mvc.service;

import com.sishuok.mvc.entity.User;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {

    public User findById(Long id) {
        User user = new User();
        user.setId(id);
        user.setName("zhang");
        return user;
    }
}
 

AbstractClientTest测试基类

public abstract class AbstractClientTest {

    static RestTemplate restTemplate;
    ObjectMapper objectMapper; //JSON
    Jaxb2Marshaller marshaller; //XML
    String baseUri = "http://localhost:8080/users";

    @Before
    public void setUp() throws Exception {
        objectMapper = new ObjectMapper(); //需要添加jackson jar包

        marshaller = new Jaxb2Marshaller(); //需要添加jaxb2实现(如xstream)
        marshaller.setPackagesToScan(new String[] {"com.sishuok"});
        marshaller.afterPropertiesSet();

        restTemplate = new RestTemplate();
    }
}
  

 

2.1 使用内嵌Jetty方式启动容器进行

需要添加jetty依赖:

        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-server</artifactId>
            <version>${jetty.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-webapp</artifactId>
            <version>${jetty.version}</version>
            <scope>test</scope>
        </dependency>

 

如果要测试JSP,请添加

        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-jsp</artifactId>
            <version>${jetty.version}</version>
            <scope>test</scope>
        </dependency>

版本:<jetty.version>8.1.8.v20121106</jetty.version>

 

 

测试示例(EmbeddedJettyClientTest.java)

public class EmbeddedJettyClientTest extends AbstractClientTest {

    private static Server server;

    @BeforeClass
    public static void beforeClass() throws Exception {
        //创建一个server
        server = new Server(8080);
        WebAppContext context = new WebAppContext();
        String webapp = "spring-mvc-test/src/main/webapp";
        context.setDescriptor(webapp + "/WEB-INF/web.xml");  //指定web.xml配置文件
        context.setResourceBase(webapp);  //指定webapp目录
        context.setContextPath("/");
        context.setParentLoaderPriority(true);

        server.setHandler(context);
        server.start();
    }

    @AfterClass
    public static void afterClass() throws Exception {
        server.stop(); //当测试结束时停止服务器
    }

    @Test
    public void testFindById() throws Exception {
        ResponseEntity<User> entity = restTemplate.getForEntity(baseUri + "/{id}", User.class, 1L);

        assertEquals(HttpStatus.OK, entity.getStatusCode());
        assertThat(entity.getHeaders().getContentType().toString(), containsString(MediaType.APPLICATION_JSON_VALUE));
        assertThat(entity.getBody(), hasProperty("name", is("zhang")));
    }
    //省略其他,请参考github
}
 

 

此处通过内嵌Jetty启动一个web容器,然后使用RestTemplate访问真实的uri进行访问,然后进行断言验证。

 

这种方式的最大的缺点是如果我只测试UserRestController,其他的组件也会加载,属于集成测试,速度非常慢。伴随着Spring Boot项目的发布,我们可以使用Spring Boot进行测试。

 

2.2 使用Spring Boot进行测试

spring boot请参考spring boot官网 和《Spring Boot——2分钟构建spring web mvc REST风格HelloWorld》进行入门。通过spring boot我们可以只加载某个控制器进行测试。更加方便。

 

添加spring-boot-starter-web依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${spring.boot.version}</version>
            <scope>test</scope>
        </dependency>

版本:<spring.boot.version>0.5.0.BUILD-SNAPSHOT</spring.boot.version>,目前还处于SNAPSHOT版本。

 

 

测试示例(SpringBootClientTest.java)

public class SpringBootClientTest extends AbstractClientTest {

    private static ApplicationContext ctx;

    @BeforeClass
    public static void beforeClass() throws Exception {
        ctx = SpringApplication.run(Config.class); //启动服务器 加载Config指定的组件
    }

    @AfterClass
    public static void afterClass() throws Exception {
        SpringApplication.exit(ctx);//退出服务器
    }


    @Test
    public void testFindById() throws Exception {
        ResponseEntity<User> entity = restTemplate.getForEntity(baseUri + "/{id}", User.class, 1L);

        assertEquals(HttpStatus.OK, entity.getStatusCode());
        assertThat(entity.getHeaders().getContentType().toString(), containsString(MediaType.APPLICATION_JSON_VALUE));
        assertThat(entity.getBody(), hasProperty("name", is("zhang")));
    }

    //省略其他,请参考github
   
    @Configuration
    @EnableAutoConfiguration
    static class Config {

        @Bean
        public EmbeddedServletContainerFactory servletContainer() {
            return new JettyEmbeddedServletContainerFactory();
        }

        @Bean
        public UserRestController userController() {
            return new UserRestController(userService());
        }

        @Bean
        public UserService userService() {
            //Mockito请参考 http://stamen.iteye.com/blog/1470066
            UserService userService = Mockito.mock(UserService.class);
            User user = new User();
            user.setId(1L);
            user.setName("zhang");
            Mockito.when(userService.findById(Mockito.any(Long.class))).thenReturn(user);
            return userService;
//            return new UserServiceImpl(); //此处也可以返回真实的UserService实现
        }
    }

}

 

通过SpringApplication.run启动一个服务器,然后Config.xml是Spring的Java配置方式,此处只加载了UserRestController及其依赖UserService,对于UserService可以通过如Mockito进行模拟/也可以注入真实的实现,Mockito请参考《单元测试系列之2:模拟利器Mockito》。可以通过EmbeddedServletContainerFactory子类指定使用哪个内嵌的web容器(目前支持:jetty/tomcat)。

 

这种方式的优点就是速度比内嵌Jetty容器速度快,但是还是不够快且还需要启动一个服务器(开一个端口),因此Spring 3.2提供了模拟Server的方式进行测试。即服务器是通过Mock技术模拟的而不是真的启动一个服务器。

 

上述两种方式对于如果服务还不存在的情况也是无法测试的,因此Mock Server进行测试时最好的选择。

 

2.3 使用Mock Service Server进行测试

通过Mock Service Server方式的优点:

不需要启动服务器;

可以在服务还没写好的情况下进行测试,这样可以进行并行开发/测试。

 

对于Mock Service Server主要操作步骤:

1、通过MockRestServiceServer创建RestTemplate的Mock Server;

2、添加客户端请求断言,即用于判断客户端请求的断言;

3、添加服务端响应,即返回给客户端的响应;

 

为了方便测试,请静态导入:

import static org.springframework.test.web.client.*;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.*;
import static org.springframework.test.web.client.response.MockRestResponseCreators.*;

 

测试示例(MockServerClientTest.java)

public class MockServerClientTest extends AbstractClientTest {

    private MockRestServiceServer mockServer;

    @Before
    public void setUp() throws Exception {
        super.setUp();
        //模拟一个服务器
        mockServer = createServer(restTemplate);
    }

    @Test
    public void testFindById() throws JsonProcessingException {
        String uri = baseUri + "/{id}";
        Long id = 1L;
        User user = new User();
        user.setId(1L);
        user.setName("zhang");
        String userJson = objectMapper.writeValueAsString(user);
        String requestUri = UriComponentsBuilder.fromUriString(uri).buildAndExpand(id).toUriString();

        //添加服务器端断言
        mockServer
                .expect(requestTo(requestUri))
                .andExpect(method(HttpMethod.GET))
                .andRespond(withSuccess(userJson, MediaType.APPLICATION_JSON));

        //2、访问URI(与API交互)
        ResponseEntity<User> entity = restTemplate.getForEntity(uri, User.class, id);

        //3.1、客户端验证
        assertEquals(HttpStatus.OK, entity.getStatusCode());
        assertThat(entity.getHeaders().getContentType().toString(), containsString(MediaType.APPLICATION_JSON_VALUE));
        assertThat(entity.getBody(), hasProperty("name", is("zhang")));

        //3.2、服务器端验证(验证之前添加的服务器端断言)
        mockServer.verify();
    }
    //省略其他,请参考github
}

  

测试步骤:

1、准备测试环境

首先创建RestTemplate,然后通过MockRestServiceServer.createServer(restTemplate)创建一个Mock Server,其会自动设置restTemplate的requestFactory为RequestMatcherClientHttpRequestFactory(restTemplate发送请求时都通过ClientHttpRequestFactory创建ClientHttpRequest)。

2、调用API

即restTemplate.getForEntity(uri, User.class, id)访问rest web service;

3、断言验证

3.1、客户端请求断言验证

如mockServer.expect(requestTo(requestUri)).andExpect(method(HttpMethod.GET)):即会验证之后通过restTemplate发送请求的uri是requestUri,且请求方法是GET;

3.2、服务端响应断言验证

首先通过mockServer.andRespond(withSuccess(new ObjectMapper().writeValueAsString(user), MediaType.APPLICATION_JSON));返回给客户端响应信息;

然后restTemplate就可以得到ResponseEntity,之后就可以通过断言进行验证了;

4、 卸载测试环境

 

对于单元测试步骤请参考:加速Java应用开发速度3——单元/集成测试+CI

 

2.4 了解测试API

 

MockRestServiceServer

用来创建模拟服务器,其提供了createServer(RestTemplate restTemplate),传入一个restTemplate即可创建一个MockRestServiceServer;在createServer中:

		MockRestServiceServer mockServer = new MockRestServiceServer();
		RequestMatcherClientHttpRequestFactory factory = mockServer.new RequestMatcherClientHttpRequestFactory();

		restTemplate.setRequestFactory(factory);
即模拟一个ClientHttpRequestFactory,然后设置回RestTemplate,这样所有发送的请求都会到这个MockRestServiceServer。拿到MockRestServiceServer后,接着就需要添加请求断言和返回响应,然后进行验证。

 

 

RequestMatcher/MockRestRequestMatchers

RequestMatcher用于验证请求信息的验证器,即RestTemplate发送的请求的URI、请求方法、请求的Body体内容等等;spring mvc测试框架提供了很多***RequestMatchers来满足测试需求;类似于《Spring MVC测试框架详解——服务端测试》中的***ResultMatchers;注意这些***RequestMatchers并不是ResultMatcher的子类,而是返回RequestMatcher实例的。Spring mvc测试框架为了测试方便提供了MockRestRequestMatchers静态工厂方法方便操作;具体的API如下:

RequestMatcher anything():即请求可以是任何东西;

RequestMatcher requestTo(final Matcher<String> matcher)/RequestMatcher requestTo(final String expectedUri)/RequestMatcher requestTo(final URI uri):请求URI必须匹配某个Matcher/uri字符串/URI;

RequestMatcher method(final HttpMethod method):请求方法必须匹配某个请求方法;

RequestMatcher header(final String name, final Matcher<? super String>... matchers)/RequestMatcher header(final String name, final String... expectedValues):请求头必须匹配某个Matcher/某些值;

ContentRequestMatchers content():获取内容匹配器,然后可以通过如contentType(String expectedContentType)进行ContentType匹配等,具体请参考javadoc;

JsonPathRequestMatchers jsonPath(String expression, Object ... args)/RequestMatcher jsonPath(String expression, Matcher<T> matcher):获取Json路径匹配器/直接进行路径匹配,具体请参考javadoc;

XpathRequestMatchers xpath(String expression, Object... args)/XpathRequestMatchers xpath(String expression, Map<String, String> namespaces, Object... args):获取Xpath表达式匹配器/直接进行Xpath表达式匹配,具体请参考javadoc;

 

ResponseCreator/MockRestResponseCreators

ResponseCreator用于创建返回给客户端的响应信息,spring mvc提供了静态工厂方法MockRestResponseCreators进行操作;具体的API如下:

DefaultResponseCreator withSuccess() :返回给客户端200(OK)状态码响应;

DefaultResponseCreator withSuccess(String body, MediaType mediaType)/DefaultResponseCreator withSuccess(byte[] body, MediaType contentType)/DefaultResponseCreator withSuccess(Resource body, MediaType contentType):返回给客户端200(OK)状态码响应,且返回响应内容体和MediaType;

DefaultResponseCreator withCreatedEntity(URI location):返回201(Created)状态码响应,并返回响应头“Location=location";

DefaultResponseCreator withNoContent() :返回204(NO_CONTENT)状态码响应;

DefaultResponseCreator withBadRequest() :返回400(BAD_REQUEST)状态码响应;

DefaultResponseCreator withUnauthorizedRequest() :返回401(UNAUTHORIZED)状态码响应;

DefaultResponseCreator withServerError() :返回500(SERVER_ERROR)状态码响应;

DefaultResponseCreator withStatus(HttpStatus status):设置自定义状态码;

 

对于DefaultResponseCreator还提供了如下API:

DefaultResponseCreator body(String content) /DefaultResponseCreator body(byte[] content)/DefaultResponseCreator body(Resource resource):内容体响应,对于String content 默认是UTF-8编码的;

DefaultResponseCreator contentType(MediaType mediaType) :响应的ContentType;

DefaultResponseCreator location(URI location) :响应的Location头;

DefaultResponseCreator headers(HttpHeaders headers):设置响应头;

 

2.5 测试示例

 

测试查找

请参考之前的testFindById;

 

测试新增

提交JSON数据进行新增

    @Test
    public void testSaveWithJson() throws Exception {
        User user = new User();
        user.setId(1L);
        user.setName("zhang");
        String userJson = objectMapper.writeValueAsString(user);

        String uri = baseUri;
        String createdLocation = baseUri + "/" + 1;

        mockServer
                .expect(requestTo(uri))  //验证请求URI
                .andExpect(jsonPath("$.name").value(user.getName())) //验证请求的JSON数据
                .andRespond(withCreatedEntity(URI.create(createdLocation)).body(userJson).contentType(MediaType.APPLICATION_JSON)); //添加响应信息


        restTemplate.setMessageConverters(Arrays.<HttpMessageConverter<?>>asList(new MappingJackson2HttpMessageConverter()));
        ResponseEntity<User> responseEntity = restTemplate.postForEntity(uri, user, User.class);

        assertEquals(createdLocation, responseEntity.getHeaders().get("Location").get(0));
        assertEquals(HttpStatus.CREATED, responseEntity.getStatusCode());
        assertEquals(user, responseEntity.getBody());

        mockServer.verify();
    }

提交XML数据进行新增

    @Test
    public void testSaveWithXML() throws Exception {
        User user = new User();
        user.setId(1L);
        user.setName("zhang");
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        marshaller.marshal(user, new StreamResult(bos));
        String userXml = bos.toString();

        String uri = baseUri;
        String createdLocation = baseUri + "/" + 1;

        mockServer
                .expect(requestTo(uri))  //验证请求URI
                .andExpect(xpath("/user/name/text()").string(user.getName())) //验证请求的JSON数据
                .andRespond(withCreatedEntity(URI.create(createdLocation)).body(userXml).contentType(MediaType.APPLICATION_XML)); //添加响应信息

        restTemplate.setMessageConverters(Arrays.<HttpMessageConverter<?>>asList(new Jaxb2RootElementHttpMessageConverter()));
        ResponseEntity<User> responseEntity = restTemplate.postForEntity(uri, user, User.class);

        assertEquals(createdLocation, responseEntity.getHeaders().get("Location").get(0));
        assertEquals(HttpStatus.CREATED, responseEntity.getStatusCode());

        assertEquals(user, responseEntity.getBody());

        mockServer.verify();
    }

  

测试修改 

    @Test
    public void testUpdate() throws Exception {
        User user = new User();
        user.setId(1L);
        user.setName("zhang");

        String uri = baseUri + "/{id}";

        mockServer
                .expect(requestTo(uri))  //验证请求URI
                .andExpect(jsonPath("$.name").value(user.getName())) //验证请求的JSON数据
                .andRespond(withNoContent()); //添加响应信息

        restTemplate.setMessageConverters(Arrays.<HttpMessageConverter<?>>asList(new MappingJackson2HttpMessageConverter()));
        ResponseEntity responseEntity = restTemplate.exchange(uri, HttpMethod.PUT, new HttpEntity<>(user), (Class) null, user.getId());

        assertEquals(HttpStatus.NO_CONTENT, responseEntity.getStatusCode());

        mockServer.verify();
    }

 

测试删除 

    @Test
    public void testDelete() throws Exception {
        String uri = baseUri + "/{id}";
        Long id = 1L;

        mockServer
                .expect(requestTo(baseUri + "/" + id))  //验证请求URI
                .andRespond(withSuccess()); //添加响应信息

        ResponseEntity responseEntity = restTemplate.exchange(uri, HttpMethod.DELETE, HttpEntity.EMPTY, (Class) null, id);
        assertEquals(HttpStatus.OK, responseEntity.getStatusCode());

        mockServer.verify();
    }

 

通过Mock Server的最大好处是不需要启动服务器,且不需要服务预先存在就可以测试;如果服务已经存在,通过Spring Boot进行测试也是个不错的选择。

 

 

再来回顾下测试步骤

1、准备测试环境

首先创建RestTemplate,然后通过MockRestServiceServer.createServer(restTemplate)创建一个Mock Server,其会自动设置restTemplate的requestFactory为RequestMatcherClientHttpRequestFactory(restTemplate发送请求时都通过ClientHttpRequestFactory创建ClientHttpRequest)。

2、调用API

即restTemplate.getForEntity(uri, User.class, id)访问rest web service;

3、断言验证

3.1、客户端请求断言验证

如mockServer.expect(requestTo(requestUri)).andExpect(method(HttpMethod.GET)):即会验证之后通过restTemplate发送请求的uri是requestUri,且请求方法是GET;

3.2、服务端响应断言验证

首先通过mockServer.andRespond(withSuccess(new ObjectMapper().writeValueAsString(user), MediaType.APPLICATION_JSON));返回给客户端响应信息;

然后restTemplate就可以得到ResponseEntity,之后就可以通过断言进行验证了;

4、 卸载测试环境

 

对于单元测试步骤请参考:加速Java应用开发速度3——单元/集成测试+CI

 

测试示例请参考

https://github.com/zhangkaitao/spring4-showcase/tree/master/spring-mvc-test/src/test/java/com/sishuok/mvc/client 

 

欢迎加入spring群134755960进行交流。

 

精品视频课程推荐

Java数据结构和算法精讲版
本课程专注于数据结构和算法的内容,使用Java来进行代码示例,不空洞的讲解概念和理论,重点放在代码的实现和示例上。 从零开始、全面系统、成体系的讲解数据结构和基本算法,循序渐进的讲述构建软件系统所常见的数据结构和算法。

Ajax+JSON基础实战视频教程
数据校验、Javascript模拟多线程、下拉列表联动、操作XML、AJAX结合JSON的操作、Json-lib的使用

高级软件架构师实战培训阶段一
内容概述:本课程专注于构建:高可扩展性、高性能、大数据量、高并发、分布式的系统架构。 从零开始、全面系统、成体系的软件架构课程,循序渐进的讲述构建上述系统架构所需要的各种技术知识和技能。
技术要点: 1:构建基本的业务功能块,基于Maven+Git+Spring mvc+spring+mybatis+ehcache+mysql+X-gen代码生成
 2:高扩展性的分布式体系架构(基于Nginx+Varnish+Memcache+ActiveMQ)
 3:NoSQL的合理使用和架构优化(基于MongoDB)
 4:分布式文件存储和架构优化(基于MogileFS)

Weblogic实战视频教程
WebLogic基础知识:WebLogic基本概念、正确安装WebLogic、建域、应用部署于JDBC选择、对WebLogic的监控和日志查看、集群的高可用性;课程目标:彻底掌握WebLogic的基本概念,在理解基本概念的基础上做到正确的安装WebLogic,根据不同的需求创建域,合理选择应用部署和JDBC配置。熟练掌握WebLogic的console监控,了解各种性能和运行指标,以及对监控结果的分析,运用集群的高可用性,对集群架设。

深入浅出学Spring Data JPA视频教程
系统、完整的学习Spring Data JPA开发的知识。包括:Spring Data JPA入门;JpaRepository基本功能 ;JpaRepository的查询;客户化扩展JpaRepository;Specifications查询。

浏览(23410)|评论(0)   交流分类:Java|笔记分类: 跟我学spring3

评论(0)
请登录后评论 登录

关于我们 | 联系我们 | 用户协议 | 私塾在线服务协议 | 版权声明 | 隐私保护

版权所有 Copyright(C)2009-2012 私塾在线学习网