🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
之前的章节我们都是使用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)