本文首发于个人网站: Spring Boot 项目中使用 Mockito http://www.javaadu.online/?p=575
Spring Boot 可以和大部分流行的测试框架协同工作: 通过 Spring JUnit 创建单元测试; 生成测试数据初始化数据库用于测试; Spring Boot 可以跟 BDD(Behavier Driven Development)工具, Cucumber 和 Spock 协同工作, 对应用程序进行测试.
进行软件开发的时候, 我们会写很多代码, 不过, 再过六个月 (甚至一年以上) 你知道自己的代码怎么运作么? 通过测试 (单元测试, 集成测试, 接口测试) 可以保证系统的可维护性, 当我们修改了某些代码时, 通过回归测试可以检查是否引入了新的 bug. 总得来说, 测试让系统不再是一个黑盒子, 让开发人员确认系统可用.
在 web 应用程序中, 对 Controller 层的测试一般有两种方法:(1)发送 http 请求;(2)模拟 http 请求对象. 第一种方法需要配置回归环境, 通过修改代码统计的策略来计算覆盖率; 第二种方法是比较正规的思路, 但是在我目前经历过的项目中用得不多, 今天总结下如何用 Mock 对象测试 Controller 层的代码.
在之前的几篇文章中, 我们都使用 bookpub 这个应用程序作为例子, 今天也不例外, 准备测试它提供的 RESTful 接口是否能返回正确的响应数据. 这种测试不同于单元测试, 需要为之初始化完整的应用程序上下文, 所有的 spring bean 都织入以及数据库中需要有测试数据, 一般来说这种测试称之为集成测试或者接口测试.
实战
通过 spirng.io 新建的 Spring Boot 项目提供了一个空的测试文件 --BookPubApplicationTest.java, 内容是:
- @RunWith(SpringJUnit4ClassRunner.class)
- @SpringApplicationConfiguration(classes = BookPubApplication.class)
- public class BookPubApplicationTests {
- @Test
- public void contextLoads() {
- }
- }
在 pom 文件中增加 spring-boot-starter-test 依赖, 添加 jsonPath 依赖
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>com.jayway.jsonpath</groupId>
- <artifactId>JSON-path</artifactId>
- </dependency>
在 BookPubApplicationTest 中添加测试用例
- package com.test.bookpub;
- import com.test.bookpub.domain.Book;
- import com.test.bookpub.repository.BookRepository;
- import org.junit.Before;import org.junit.Test;
- import org.junit.runner.RunWith;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.beans.factory.annotation.Value;
- import org.springframework.boot.test.SpringApplicationConfiguration;
- import org.springframework.boot.test.TestRestTemplate;
- import org.springframework.boot.test.WebIntegrationTest;
- import org.springframework.http.MediaType;
- import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
- import org.springframework.test.Web.servlet.MockMvc;
- import org.springframework.test.Web.servlet.setup.MockMvcBuilders;
- import org.springframework.Web.client.RestTemplate;
- import org.springframework.Web.context.WebApplicationContext;
- import static org.junit.Assert.assertEquals;
- import static org.junit.Assert.assertNotNull;
- import static org.hamcrest.Matchers.containsString;
- import static org.springframework.test.Web.servlet.request.MockMvcRequestBuilders.get;
- import static org.springframework.test.Web.servlet.result.MockMvcResultMatchers.content;
- import static org.springframework.test.Web.servlet.result.MockMvcResultMatchers.jsonPath;
- import static org.springframework.test.Web.servlet.result.MockMvcResultMatchers.status;
- @RunWith(SpringJUnit4ClassRunner.class)
- @SpringApplicationConfiguration(classes = BookPubApplication.class)
- @WebIntegrationTest("server.port:0")
- public class BookPubApplicationTests {
- @Autowired
- private WebApplicationContext context;
- @Autowired
- private BookRepository bookRepository;
- @Value("${local.server.port}")
- private int port;
- private MockMvc mockMvc;
- private RestTemplate restTemplate = new TestRestTemplate();
- @Before
- public void setupMockMvc() {
- mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
- }
- @Test
- public void contextLoads() {
- assertEquals(1, bookRepository.count());
- }
- @Test
- public void webappBookIsbnApi() {
- Book book = restTemplate.getForObject("http://localhost:" + port +"/books/9876-5432-1111", Book.class);
- assertNotNull(book);
- assertEquals("中文测试", book.getPublisher().getName());
- }
- @Test
- public void webappPublisherApi() throws Exception {
- //MockHttpServletRequestBuilder.accept 方法是设置客户端可识别的内容类型
- //MockHttpServletRequestBuilder.contentType, 设置请求头中的 Content-Type 字段, 表示请求体的内容类型
- mockMvc.perform(get("/publishers/1")
- .accept(MediaType.APPLICATION_JSON_UTF8))
- .andExpect(status().isOk())
- .andExpect(content().string(containsString("中文测试")))
- .andExpect(jsonPath("$.name").value("中文测试"));
- }
- }
spring boot 项目的代码覆盖率
使用 cobertura, 参考项目的 GitHub 地址: spring boot template https://github.com/duqicauc/spring-template
- # To create test coverage reports (in target/site/cobertura)
- mvn clean cobertura:cobertura test
分析
首先分析在 BookPubApplicationTests 类中用到的注解:
@RunWith(SpringJUnit4ClassRunner.class), 这是 JUnit 的注解, 通过这个注解让 SpringJUnit4ClassRunner 这个类提供 Spring 测试上下文.
@SpringApplicationConfiguration(classes = BookPubApplication.class), 这是 Spring Boot 注解, 为了进行集成测试, 需要通过这个注解加载和配置 Spring 应用上下文. 这是一个元注解 (meta-annoation), 它包含了 @ContextConfiguration( loader = SpringApplicationContextLoader.class) 这个注解, 测试框架通过这个注解使用 Spring Boot 框架的 SpringApplicationContextLoader 加载器创建应用上下文.
@WebIntegrationTest("server.port:0"), 这个注解表示当前的测试是集成测试 (integration test), 因此需要初始化完整的上下文并启动应用程序. 这个注解一般和 @SpringApplicationConfiguration 一起出现. server.port:0 指的是让 Spring Boot 在随机端口上启动 Tomcat 服务, 随后在测试中程序通过 @Value("${local.server.port}") 获得这个端口号, 并赋值给 port 变量. 当在 Jenkins 或其他持续集成服务器上运行测试程序时, 这种随机获取端口的能力可以提供测试程序的并行性.
了解完测试类的注解, 再看看测试类的内部. 由于这是 Spring Boot 的测试, 因此我们可通过 @Autowired 注解织入任何由 Spring 管理的对象, 或者是通过 @Value 设置指定的环境变量的值. 在现在这个测试类中, 我们定义了 WebApplicationContext 和 BookRepository 对象.
每个测试用例用 @Test 注解修饰. 在第一个测试用例 --contextLoads()方法中, 我仅仅需要确认 BookRepository 连接已经建立, 并且数据库中已经包含了对应的测试数据.
第二个测试用例用来测试我们提供的 RESTful URL-- 通过 ISBN 查询一本书, 即 "/books/{isbn}". 在这个测试用例中我们使用 TestRestTemplate 对象发起 RESTful 请求.
第三个测试用例中展示了如何通过 MockMvc 对象实现跟第二个测试类似的功能. Spring 测试框架提供 MockMvc 对象, 可以在不需要客户端 - 服务端请求的情况下进行 MVC 测试, 完全在服务端这边就可以执行 Controller 的请求, 跟启动了测试服务器一样.
测试开始之前需要建立测试环境, setup 方法被 @Before 修饰. 通过 MockMvcBuilders 工具, 使用 WebApplicationContext 对象作为参数, 创建一个 MockMvc 对象.
MockMvc 对象提供一组工具函数用来执行 assert 判断, 都是针对 Web 请求的判断. 这组工具的使用方式是函数的链式调用, 允许程序员将多个测试用例链接在一起, 并进行多个判断. 在这个例子中我们用到下面的一些工具函数:
perform(get(...))建立 Web 请求. 在我们的第三个用例中, 通过 MockMvcRequestBuilder 执行 GET 请求.
andExpect(...)可以在 perform(...)函数调用后多次调用, 表示对多个条件的判断, 这个函数的参数类型是 ResultMatcher 接口, 在 MockMvcResultMatchers 这这个类中提供了很多返回 ResultMatcher 接口的工具函数. 这个函数使得可以检测同一个 Web 请求的多个方面, 包括 HTTP 响应状态码 (response status), 响应的内容类型(content type), 会话中存放的值, 检验重定向, model 或者 header 的内容等等. 这里需要通过第三方库 JSON-path 检测 JSON 格式的响应数据: 检查 JSON 数据包含正确的元素类型和对应的值, 例如 jsonPath("$.name").value("中文测试") 用于检查在根目录下有一个名为 name 的节点, 并且该节点对应的值是 "中文测试".
一个字符乱码问题
问题描述: 通过 spring-boot-starter-data-REST 建立的 repository, 取出的汉字是乱码.
分析: 使用 postman 和 httpie 验证都没问题, 说明是 Mockmvc 的测试用例写得不对, 应该主动设置客户端如何解析 HTTP 响应, 用 get.accept 方法设置客户端可识别的内容类型, 修改后的测试用例如下:
- @Test
- public void webappPublisherApi() throws Exception {
- //MockHttpServletRequestBuilder.accept 方法是设置客户端可识别的内容类型
- //MockHttpServletRequestBuilder.contentType, 设置请求头中的 Content-Type 字段, 表示请求体的内容类型
- mockMvc.perform(get("/publishers/1")
- .accept(MediaType.APPLICATION_JSON_UTF8))
- .andExpect(status().isOk())
- .andExpect(content().string(containsString("中文测试")))
- .andExpect(jsonPath("$.name").value("中文测试"));
- }
参考资料
基于 Spring-WS 的 Restful API 的集成测试 http://ningandjiao.iteye.com/blog/1982635
J2EE 要懂的小事 - 图解 HTTP 协议 http://www.bysocket.com/?p=282
- Integration Testing a Spring Boot Application
- spring boot project template
Spring Boot 1.x 系列
Spring Boot 的自动配置, Command-line-Runner http://www.javaadu.online/?p=487
了解 Spring Boot 的自动配置 http://www.javaadu.online/?p=495
Spring Boot 的 @PropertySource 注解在整合 Redis 中的使用 http://www.javaadu.online/?p=499
Spring Boot 项目中如何定制 HTTP 消息转换器 http://www.javaadu.online/?p=515
Spring Boot 整合 MongoDB 提供 Restful 接口 http://www.javaadu.online/?p=518
Spring 中 bean 的 scope http://www.javaadu.online/?p=521
Spring Boot 项目中使用事件派发器模式 http://www.javaadu.online/?p=526
Spring Boot 提供 RESTful 接口时的错误处理实践 http://www.javaadu.online/?p=530
Spring Boot 实战之定制自己的 starter http://www.javaadu.online/?p=535
Spring Boot 项目如何同时支持 HTTP 和 HTTPS 协议 http://www.javaadu.online/?p=538
自定义的 Spring Boot starter 如何设置自动配置注解 http://www.javaadu.online/?p=546
本号专注于后端技术, JVM 问题排查和优化, Java 面试题, 个人成长和自我管理等主题, 为读者提供一线开发者的工作和成长经验, 期待你能在这里有所收获.
来源: https://www.cnblogs.com/javaadu/p/11748415.html