之前的章节我们都是使用postman来进行接口测试的,本节我们来教大家使用编码的方式来进行接口测试。
在开始测试之前我们调整一下代码(方便测试):
![](https://img.kancloud.cn/9a/a3/9aa3642145cfab32974a3dd678f0ad95_1887x269.png)
使用Postman的测试返回结果如下:
![](https://img.kancloud.cn/bf/0c/bf0cf1288f1ad17c4bb456fb0c9b1c24_1071x650.png)
## 一、编码实现接口测试
#### 1.1.问:为什么要写代码做测试?使用接口测试工具Postman很方便啊
答:因为在做系统的自动化持续集成的时候,会要求自动的做单元测试,只有所有的单元测试都跑通了,才能打包构建。比如:使用maven在打包之前将所有的测试用例执行一遍。这里重点是**自动化**,所以postman这种工具很难插入到持续集成的自动化流程中去。
#### 1.2.junit测试框架
在开始书写测试代码之前,我们先回顾一下JUnit常用的测试注解。在junit4和junit5中,注解的写法有些许变化。
| junit4 | junit5 | 特点 |
| --- | --- | --- |
| @Test | @Test | 声明一个测试方法 |
| @BeforeClass | @BeforeAll | 在当前类的所有测试方法之前执行。注解在【静态方法】上 |
| @AfterClass | @AfterAll | 在当前类中的所有测试方法之后执行。注解在【静态方法】上 |
| @Before | @BeforeEach | 在每个测试方法之前执行。注解在【非静态方法】上 |
| @After | @AfterEach | 在每个测试方法之后执行。注解在【非静态方法】 |
| @RunWith(SpringRunner.class) | @ExtendWith(SpringExtension.class) | 类class定义上 |
#### 1.3.Mockito测试框架
Mockito是GitHub上使用最广泛的Mock框架,并与JUnit结合使用.Mockito框架可以创建和配置mock对象.使用Mockito简化了具有外部依赖的类的测试开发。Mockito测试框架可以帮助我们模拟HTTP请求,从而达到在服务端测试目的。因为其不会真的去发送HTTP请求,而是模拟HTTP请求内容,从而节省了HTTP请求的网络传输,测试速度更快。
![](https://img.kancloud.cn/49/e6/49e69a8d69054466f98be13e22c60e12_1062x364.png)
~~~
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
~~~
> spring-boot-starter-test(Spring Boot 2.3.0.RELEASE)自动包含Junit 5 和Mockito框架,以下测试代码是基于Junit5,使用Junit4的同学请自行调整代码。
~~~
@Slf4j
public class ArticleRestControllerTest {
//mock对象
private static MockMvc mockMvc;
//在所有测试方法执行之前进行mock对象初始化
@BeforeAll
static void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(new ArticleController()).build();
}
//测试方法
@Test
public void saveArticle() throws Exception {
String article = "{\n" +
" \"id\": 1,\n" +
" \"author\": \"zimug\",\n" +
" \"title\": \"手摸手教你开发spring boot\",\n" +
" \"content\": \"c\",\n" +
" \"createTime\": \"2017-07-16 05:23:34\",\n" +
" \"reader\":[{\"name\":\"zimug\",\"age\":18},{\"name\":\"kobe\",\"age\":37}]\n" +
"}";
MvcResult result = mockMvc.perform(
MockMvcRequestBuilders
.request(HttpMethod.POST, "/rest/articles")
.contentType("application/json")
.content(article)
)
.andExpect(MockMvcResultMatchers.status().isOk()) //HTTP:status 200
.andExpect(MockMvcResultMatchers.jsonPath("$.data.author").value("zimug"))
.andExpect(MockMvcResultMatchers.jsonPath("$.data.reader[0].age").value(18))
.andDo(print())
.andReturn();
result.getResponse().setCharacterEncoding("UTF-8");
log.info(result.getResponse().getContentAsString());
}
}
~~~
MockMvc对象有以下几个基本的方法:
* perform : 模拟执行一个RequestBuilder构建的HTTP请求,会执行SpringMVC的流程并映射到相应的控制器Controller执行。
* contentType:发送请求内容的序列化的格式,"application/json"表示JSON数据格式
* andExpect: 添加RequsetMatcher验证规则,验证控制器执行完成后结果是否正确,或者说是结果是否与我们期望(Expect)的一致。
* andDo: 添加ResultHandler结果处理器,比如调试时打印结果到控制台
* andReturn: 最后返回相应的MvcResult,然后进行自定义验证/进行下一步的异步处理
> 上面的整个过程,我们都没有使用到Spring Context依赖注入、也没有启动tomcat web容器。整个测试的过程十分的轻量级,速度很快。
## 二、真实servlet容器环境下的测试
上面的测试执行速度非常快,但是有一个问题:它没有启动servlet容器和Spring 上下文,自然也就无法实现依赖注入(不支持@Resource和@AutoWired注解)。这就导致它在从控制层到持久层全流程测试中有很大的局限性。
![](https://img.kancloud.cn/51/16/51161d5ade8b67cc8a4d10e0740094e8_1075x296.png)
换一种写法:看看有没有什么区别。在测试类上面额外加上这样两个注解,并且mockMvc对象使用@Resource自动注入,删掉Before注解及setUp函数。
~~~
@AutoConfigureMockMvc
@SpringBootTest
@ExtendWith(SpringExtension.class)
~~~
![](https://img.kancloud.cn/8b/19/8b191e40c66f95316a45a1f8f7b60ed4_1285x516.png)
启动测试一下,看看和之前有没有什么区别.
![](https://box.kancloud.cn/e6994b5795a3afddf38eb85e83932a89_1603x263.png)
看到上面这个截图,是不是已经明白了!该测试方法真实的启动了一个tomcat容器、以及Spring 上下文,所以我们可以进行依赖注入(@Resource)。实现的效果和使用MockMvcBuilders构建MockMVC对象的效果是一样的,但是有一个非常明显的缺点:每次做一个接口测试,都会真实的启动一次servlet容器,Spring上下文加载项目里面定义的所有的Bean,导致执行过程很缓慢。
### 2.1 @SpringBootTest 注解
是用来创建Spring的上下文ApplicationContext,保证测试在上下文环境里运行。单独使用@SpringBootTest不会启动servlet容器。所以**只是使用SpringBootTest 注解,不可以使用@Resource和@Autowired等注解进行bean的依赖注入**。(准确的说是可以使用,但被注解的bean为null)。
### 2.2 @ExtendWith(@RunWith注解)
* RunWith方法为我们构造了一个的Servlet容器运行运行环境,并在此环境下测试。然而为什么要构建servlet容器?因为使用了依赖注入,注入了MockMvc对象,而在上一个例子里面是我们自己new的。
* 而@AutoConfigureMockMvc注解,该注解表示mockMvc对象由spring 依赖注入构建,你只负责使用就可以了。这种写法是为了让测试在servlet容器环境下执行。
简单的说:**如果你单元测试代码使用了“依赖注入@Resource”就必须加上@ExtendWith,如果你不是手动new MockMvc对象就加上@AutoConfigureMockMvc**
### 2.3 @Transactional
该注解加在方法上可以使单元测试进行事务回滚,以保证数据库表中没有因测试造成的垃圾数据,因此保证单元测试可以反复执行;
但是笔者不建议这么做,使用该注解会破坏测试真实性。请参考这篇文章详细理解:
[不要在 Spring Boot 集成测试中使用 @Transactional](http://www.zimug.com/other/springboot/%e4%b8%8d%e8%a6%81%e5%9c%a8spring%e5%8d%95%e5%85%83%e6%b5%8b%e8%af%95%e4%b8%ad%e4%bd%bf%e7%94%a8-transactional%e6%b3%a8%e8%a7%a3/.html)
## 三、Mock测试
### 3.1什么是Mock?
在面向对象程序设计中,模拟对象(英语:mock object,也译作模仿对象)是以可控的方式模拟真实对象行为的**假的对象**。比如:对象B依赖于对象A,但是A代码还没写是一个空类空方法不能用,我们来mock一个假的A来完成测试。
### 3.2 为什么要使用Mock?
![](https://img.kancloud.cn/4e/70/4e7030c801cf14348b7e2a2287e04144_764x488.png)
> 在单元测试中,模拟对象可以模拟复杂的、真实的对象的行为, 如果真实的对象无法放入单元测试中,使用模拟对象就很有帮助。
在下面的情形,可能需要使用**"模拟对象行为"**来代替真实对象:
* 真实对象的行为是不确定的(例如,当前的时间或当前的温度);
* 真实对象很难搭建起来;
* 真实对象的行为很难触发(例如,网络错误);
* 真实对象速度很慢(例如,一个完整的数据库,在测试之前可能需要初始化);
* 真实的对象是用户界面,或包括用户界面在内;
* 真实的对象使用了回调机制;
* 真实对象可能还不存在(例如,其他程序员还为完成工作);
* 真实对象可能包含不能用作测试的信息(高度保密信息等)和方法。
### 3.3.场景实践
我们的保存文章的Controller方法,调用ArticleService的saveArticle进行文章的保存。
![](https://img.kancloud.cn/99/15/9915f4d54a24e5a4ab3b99e9d847aa4a_1161x209.png)
但是因为种种原因,这个接口目前没能实现(只有接口,代码如下)。比如说:另一个程序员暂时没完成工作,或者是机密内容实现,不能被用于测试环境。
~~~
public interface ArticleService {
public String saveArticle(Article article);
}
~~~
但是现在接口调用方找到我了,需要进行接口验证。怎么办?我们就可以使用Mock的方法,先Mock一个假的ArticleService,把接口验证完成。
~~~
@Slf4j
@AutoConfigureMockMvc
@SpringBootTest
@ExtendWith(SpringExtension.class)
public class ArticleRestControllerTest3 {
//mock对象
@Resource
private MockMvc mockMvc;
@MockBean
private ArticleService articleService;
//测试方法
@Test
public void saveArticle() throws Exception {
String article = "{\n" +
" \"id\": 1,\n" +
" \"author\": \"zimug\",\n" +
" \"title\": \"手摸手教你开发spring boot\",\n" +
" \"content\": \"c\",\n" +
" \"createTime\": \"2017-07-16 05:23:34\",\n" +
" \"reader\":[{\"name\":\"zimug\",\"age\":18},{\"name\":\"kobe\",\"age\":37}]\n" +
"}";
ObjectMapper objectMapper = new ObjectMapper();
Article articleObj = objectMapper.readValue(article,Article.class);
//打桩
when(articleService.saveArticle(articleObj)).thenReturn("ok");
MvcResult result = mockMvc.perform(
MockMvcRequestBuilders.request(HttpMethod.POST, "/rest/articles")
.contentType("application/json").content(article))
.andExpect(MockMvcResultMatchers.jsonPath("$.data").value("ok"))
.andDo(print())
.andReturn();
result.getResponse().setCharacterEncoding("UTF-8");
log.info(result.getResponse().getContentAsString());
}
}
~~~
### @MockBean
可以用MockBean伪造模拟一个Service ,如上图中的MockBean。
大家注意上文代码中,打了一个桩
~~~
when(articleService.saveArticle(articleObj)).thenReturn("ok");
~~~
也就是告诉测试用例程序,当你调用articleService.saveArticle(articleObj)方法的时候,不要去真的调用这个方法,直接返回一个结果(“ok”)就好了。
~~~
.andExpect(MockMvcResultMatchers.jsonPath("$.data").value("ok"))
~~~
测试用例跑通了,期望结果andExpect:ok与实际结果thenReturn("ok")一致。表示程序真正的去执行了MockBean的模拟行为,而不是调用真实对象的方法。
## 四、轻量级测试
在ExtendWith的AutoConfigureMockMvc注解的共同作用下,启动了SpringMVC的运行容器,并且把项目中所有的@Bean全部都注入进来。把所有的bean都注入进来是不是很臃肿?这样会拖慢单元测试的效率。如果我只是想测试一下控制层Controller,怎么办?或者说我只想具体到测试一下ArticleRestController,怎么办?要把应用中所有的bean都注入么?有没有轻量级的解决方案?一定是有的。
~~~
@ExtendWith(SpringExtension.class)
@WebMvcTest(ArticleController.class)
//@SpringBootTest
~~~
#### 使用@WebMvcTest替换@SpringBootTest
* @SpringBootTest注解告诉SpringBoot去寻找一个主配置类(例如带有@SpringBootApplication的配置类),并使用它来启动Spring应用程序上下文。SpringBootTest加载完整的应用程序并注入所有可能的bean,因此速度会很慢。
* @WebMvcTest注解主要用于controller层测试,只覆盖应用程序的controller层,@WebMvcTest(ArticleController.class)只加载ArticleController这一个Bean用作测试。所以WebMvcTest要快得多,因为我们只加载了应用程序的一小部分。
## 五、MockMvc更多的用法总结
~~~
//模拟GET请求:
mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", userId));
//模拟Post请求:
mockMvc.perform(MockMvcRequestBuilders.post("uri", parameters));
//模拟文件上传:
mockMvc.perform(MockMvcRequestBuilders.multipart("uri").file("fileName", "file".getBytes("UTF-8")));
//模拟session和cookie:
mockMvc.perform(MockMvcRequestBuilders.get("uri").sessionAttr("name", "value"));
mockMvc.perform(MockMvcRequestBuilders.get("uri").cookie(new Cookie("name", "value")));
//设置HTTP Header:
mockMvc.perform(MockMvcRequestBuilders
.get("uri", parameters)
.contentType("application/x-www-form-urlencoded")
.accept("application/json")
.header("", ""));
~~~
附:
踩坑情况,字符集乱码失效的时候可以用第二种办法,虽然是已过时的。
![](https://img.kancloud.cn/19/36/193660eba96dc3578ac8923bf55f5b75_1185x744.png)
- 内容简介
- 第一章 Spring boot 简介
- 1.1 helloworld
- 1.2 提高开发效率工具lombok
- 1.3 IDEA热部署
- 1.4 IDEA常用插件
- 1.5 常用注解
- 第二章 RESTful接口
- 2.1 RESTful风格API
- 2.1.1 spring常用注解开发RESTful接口
- 2.1.2 HTTP协议与Spring参数接收注解
- 2.1.3 Spring请求处理流程注解
- 2.2 JSON数据格式处理
- 2.2.1 Jackson的转换示例代码
- 2.3 针对接口编写测试代码
- 2.3.1 编码接口测试示例代码
- 2.3.2 带severlet容器的接口测试示例代码
- 2.3.3 Mockito测试示例代码
- 2.3.4 Mockito轻量测试
- 2.4 使用swagger2构建API文档
- 2.4.1 swagger2示例代码
- 2.4.2 pom.xml
- 2.5 使用swagger2导出各种格式的接口文档
- 第三章 sping boot配置管理
- 3.1 YAML语法
- 3.2 YAML绑定配置变量的方式
- 3.3 YAML配置属性值校验
- 3.4 YAML加载外部配置文件
- 3.5 SpEL表达式绑定配置项
- 3.6 不同环境下的多配置
- 3.7 配置文件的优先级
- 3.8 配置文件敏感字段加密
- 第四章 连接数据库使用到的框架
- 4.1 spring JDBC
- 4.2 mybatis配置mybatisgenerator自动生成代码
- 4.3 mybatis操作数据库+dozer整合Bean自动加载
- 4.4 spring boot mybatis 规范
- 4.5 spirng 事务与分布式事务
- 4.6 spring mybaits 多数据源(未在git版本中实现)
- 4.7 mybatis+atomikos实现分布式事务(未在git版本中实现)
- 4.8 mybatis踩坑之逆向工程导致的服务无法启动
- 4.9 Mybatis Plus
- 4.9.1.CURD快速入门
- 4.9.2.条件构造器使用与总结
- 4.9.3.自定义SQL
- 4.9.4.表格分页与下拉分页查询
- 4.9.5.ActiveRecord模式
- 4.9.6.主键生成策略
- 4.9.7.MybatisPlus代码生成器
- 4.9.8.逻辑删除
- 4.9.9.字段自动填充
- 4.9.10.多租户解决方案
- 4.9.11.雪花算法与精度丢失
- 第五章 页面展现整合
- 5.1 webjars与静态资源
- 5.2 模板引擎与未来趋势
- 5.3 整合JSP
- 5.4 整合Freemarker
- 5.5 整合Thymeleaf
- 5.6 Thymeleaf基础语法
- 5.7 Thymeleaf内置对象与工具类
- 5.8 Thymeleaf公共片段(标签)和内联JS
- 第六章 生命周期内的拦截、监听
- 6.1 servlet与filter与listener的实现
- 6.1.1 FilterRegistration
- 6.1.2 CustomFilter
- 6.1.3 Customlister
- 6.1.4 FirstServlet
- 6.2 spring拦截器及请求链路说明
- 6.2.1 MyWebMvcConfigurer
- 6.2.2 CustomHandlerInterceptor
- 6.3 自定义事件的发布与监听
- 6.4 应用启动的监听
- 第七章 嵌入式容器的配置与应用
- 7.1 嵌入式的容器配置与调整
- 7.2 切换到jetty&undertow容器
- 7.3 打war包部署到外置tomcat容器
- 第八章 统一全局异常处理
- 8.1 设计一个优秀的异常处理机制
- 8.2 自定义异常和相关数据结构
- 8.3 全局异常处理ExceptionHandler
- 8.3.1 HelloController
- 8.4 服务端数据校验与全局异常处理
- 8.5 AOP实现完美异常处理方案
- 第九章 日志框架与全局日志管理
- 9.1 日志框架的简介与选型
- 9.2 logback日志框架整合使用
- 9.3 log4j2日志框架整合与使用
- 9.4 拦截器实现用户统一访问日志
- 第十章 异步任务与定时任务
- 10.1 实现Async异步任务
- 10.2 为异步任务规划线程池
- 10.3 通过@Scheduled实现定时任务
- 10.4 quartz简单定时任务(内存持久化)
- 10.5 quartz动态定时任务(数据库持久化)
- 番外章节
- 1.windows下安装git
- 1 git的使用
- 2 idea通过git上传代码到github
- 2.maven配置
- 3.idea几个辅助插件
- 4.idea配置数据库
- 5.搭建外网穿透实现外网访问内网项目
- 6.idea设置修改页面自动刷新
- 7.本地tomcat启动乱码
- 8.win10桌面整理,得到一个整洁的桌面
- 9.//TODO的用法
- 10.navicat for mysql 工具激活
- 11.安装redis
- 12.idea修改内存
- 13.IDEA svn配置
- 14.IntelliJ IDEA像Eclipse一样打开多个项目