作为后端开发,我们很容易就忽略了测试用例的重要性,就好像我们容易忽略注释一样,这些我们看似并不重要的东西,其实贯穿于我们整个功能的健壮性以及后续的维护,所以今天我们就来讲讲 springboot 上的测试用例怎么写,怎么用。
前言 在进行下面测试用例的学习前,我们需要先准备一个 springboot 工程。这边我们从https://start.spring.io/
直接搞一个
快速搞个 SSMP 为了我们下面的测试,我们将上面的 springboot-testcase-demo
工程模拟一个 restful 接口,与数据库的用户表交互
第一步:执行 SQL 脚本,生成测试用的用户表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 CREATE DATABASE IF NOT EXISTS ssmp;DROP TABLE IF EXISTS tbl_user;create table tbl_user ( ` id` int auto_increment comment ' 主键' primary key, ` name` varchar (32 ) null comment '姓名' , age int null comment '年龄' , remarks varchar (128 ) null comment ' 简介' ) comment '用户表' ;create index `tbl_user_ id_index` on tbl_user (` id`);
第二步:集成Mybatis-plus
组件
在pom.xml
中添加依赖
1 2 3 4 5 <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.5.3</version > </dependency >
逆向工程生成对应的mapper
已经实体类以及服务层(这个步骤就不再赘述)
综上,这样我们就准备好了测试用例的示例工程,下面让我们开始测试用例的学习之旅~
测试用例 模块一:加载测试专用属性 举一个例子,假设我们现在开发了一个 A 功能,然后功能有个属性 a.isOpen
用来控制 A 功能是否开启。当我们开发完之后,想要用测试用例去调试我们的功能,这时候我们可能需要引入专用的测试用的属性,显然如果这时候我们用application.yml
来校验也行,但是这样就会污染了正式的配置文件,所以我们就需要能够单独引入测试专用属性的方法。
在test
目录新建一个测试类ATest.java
,加载测试用的application.properties
的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @SpringBootTest(properties = {"a.isOpen=true"}) @DisplayName("A功能测试类") public class ATest { @Value("${a.isOpen}") private boolean isOpen; @Test @DisplayName("测试a功能是否可以获取专用属性") void testA () { if (isOpen) { System.out.println("获取成功!" ); } } }
在test
目录新建一个测试类BTest.java
,加载测试用启动传入的args
命令行参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @SpringBootTest(args = {"--server.port=9999"}) @DisplayName("B 功能测试类") public class BTest { @Value("${server.port}") private int port; @Test @DisplayName("测试 b 功能获取传入参数") void testB () { System.out.println("port = " + port); } }
模块二:加载测试专用配置 基于上面的加载专用属性的场景下,如果我们要加载更多的自定义的测试属性,那我们又该如何去做呢?
在test
目录下,新建一个配置类MsgConfig.java
1 2 3 4 5 6 7 8 9 @Configuration public class MsgConfig { @Bean public String msg () { return "hello world" ; } }
新建一个测试类CTest.java
1 2 3 4 5 6 7 8 9 10 11 @SpringBootTest @Import({MsgConfig.class}) @DisplayName("C功能测试类") public class CTest { @Test @DisplayName("测试c 功能的加载临时配置类") void testMsg (@Autowired String msg) { System.out.println("msg = " + msg); } }
模块三:启动 web环境 在我们上面的测试用例中,实际上都是一些非 web 方式的注解介绍,那么对于控制层的请求,我们是否能够通过 springboot 提供的测试组件来模拟呢?答案肯定是可以的,那么假如我们想要测试 restful
接口,首先我们需要让测试类能够启动一个 web 环境,只有在 web 环境下我们才能够对控制层的请求进行测试。
既然是要在测试类里启动 web 环境,那么我们还是从@SpringBootTest
注解入手,看看他的属性除了能让我们加载测试专用的参数,是否还有我们所需要的 web 环境。
我们可以从以上SpringBootTest
源码中看出,确实除了指定获取参数,我们还可以模拟出一个 web 环境,具体我们用代码来操作一下。
下面,我们写一个测试类Dtest.java
,然后我们启动它来看一下控制台的输出
1 2 3 4 5 6 7 8 9 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @DisplayName("D功能测试类") public class DTest { @Test void test () { System.out.println("true = " + true ); } }
我们可以看出确实是启动了一个 web 环境,当然除了使用我们自定义端口外,它还提供了以下几种
SpringBootTest.WebEnvironment.RANDOM_PORT 以随机端口号启动
SpringBootTest.WebEnvironment.MOCK 提供一个 mock的 servlet 环境,内置的servlet 容器并没有启动,主要是搭配@AutoConfigureMockMvc
SpringBootTest.WebEnvironment.NONE 不提供真实的 servlet 环境
既然我们有了 web 环境,那么我们下一步就可以来模拟请求并进行测试了。
模块四:模拟请求 既然我们要模拟请求,那么我们首先就需要在我们的示例工程里写一个控制层,比如UserController.java
1 2 3 4 5 6 7 8 9 @RestController @RequestMapping("/user") public class UserController { @GetMapping("/test") public String test () { System.out.println("this is test" ); return null ; } }
然后,我们就可以创建一个测试类Etest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc @DisplayName("E功能测试类") public class ETest { @Test @DisplayName("测试模拟请求") void test (@Autowired MockMvc mvc) throws Exception { RequestBuilder builder = MockMvcRequestBuilders.get("/user/test" ); ResultActions actions = mvc.perform(builder); } }
模块五:模拟请求-匹配响应执行状态 在我们做测试用例时,我们当然不能仅仅发起请求就结束了,那我们接下来就先说如何匹配响应执行状态,是否真实返回的状态与我们的预期值一致呢?
首先,我们要先知道一个类,叫做MockMvcResultMatchers
,即结果匹配器。毋庸置疑,我们就是用这个匹配器就是帮我们设置各种各样的预期值,来与真实值做比对,那下面我们就上手来用一下这个匹配器。
基于上面的ETest.java
测试类,我们添加如下的方法来验证响应状态。
1 2 3 4 5 6 7 8 9 10 11 12 @Test @DisplayName("测试模拟请求的响应状态") void testStatus (@Autowired MockMvc mvc) throws Exception { RequestBuilder builder = MockMvcRequestBuilders.get("/user/test" ); ResultActions actions = mvc.perform(builder); StatusResultMatchers status = MockMvcResultMatchers.status(); ResultMatcher ok = status.isOk(); actions.andExpect(ok); }
结合上面我们准备的接口,我们可以知道我们的这个请求肯定是没有问题的,那这样的话就看不出预期值和真实值的效果了,所以我们通过修改/user/test
成一个不存在的请求接口,比如/user/test000
,基于这样的改造,我们再来看下测试用例的执行结果是怎么样的,从而我们更好的了解它的一个比对过程。
1 2 3 4 5 6 7 8 9 10 11 12 @Test @DisplayName("测试模拟请求的响应状态") void testStatus (@Autowired MockMvc mvc) throws Exception { RequestBuilder builder = MockMvcRequestBuilders.get("/user/test000" ); ResultActions actions = mvc.perform(builder); StatusResultMatchers status = MockMvcResultMatchers.status(); ResultMatcher ok = status.isOk(); actions.andExpect(ok); }
结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 MockHttpServletRequest: HTTP Method = GET Request URI = /user/test000 Parameters = {} Headers = [] Body = null Session Attrs = {} Handler: Type = org.springframework.web.servlet.resource.ResourceHttpRequestHandler Async: Async started = false Async result = null Resolved Exception: Type = null ModelAndView: View name = null View = null Model = null FlashMap: Attributes = null MockHttpServletResponse: Status = 404 Error message = null Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"] Content type = null Body = Forwarded URL = null Redirected URL = null Cookies = [] java.lang.AssertionError: Status expected:<200> but was:<404> Expected :200 Actual :404
通过上面的日志结果,我们可以看到两个值的对比,所以通过这个结果匹配器就可以对我们的响应状态做校验,那么既然它叫做结果匹配器,那是不是也可以对我们的响应体内容进行比对呢,答案是显而易见的,我们继续往下走。
模块六:模拟请求-匹配响应体 实际上,匹配响应体跟我们的匹配响应状态是差不多的,话不多说,我们下面直接来改造代码。
首先,我们先改造原本的/user/test
接口,让它有个响应体。
1 2 3 4 5 @GetMapping("/test") public String test () { System.out.println("this is test" ); return "success" ; }
我们将接口改造好之后,我们就可以在ETest.java
加上一个测试方法了。(因为成功的看不出啥东西,所以我们这里直接模拟个错误的响应体,我们直接来看看匹配过程。)
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test @DisplayName("测试模拟请求的响应体") void testContent (@Autowired MockMvc mvc) throws Exception { RequestBuilder builder = MockMvcRequestBuilders.get("/user/test" ); ResultActions actions = mvc.perform(builder); ContentResultMatchers content = MockMvcResultMatchers.content(); ResultMatcher matcher = content.string("success123" ); actions.andExpect(matcher); }
结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 MockHttpServletRequest: HTTP Method = GET Request URI = /user/test Parameters = {} Headers = [] Body = null Session Attrs = {} Handler: Type = com.gcoder5.controller.UserController Method = com.gcoder5.controller.UserController#test() Async: Async started = false Async result = null Resolved Exception: Type = null ModelAndView: View name = null View = null Model = null FlashMap: Attributes = null MockHttpServletResponse: Status = 200 Error message = null Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"7"] Content type = text/plain;charset=UTF-8 Body = success Forwarded URL = null Redirected URL = null Cookies = [] java.lang.AssertionError: Response content expected:<success123> but was:<success> Expected :success123 Actual :success
从最后两行,我们可以看到两者的匹配确实是我们设定的情况,所以通过这样的方式我们就可以匹配响应体结果是否正确了。当然,我们在实际业务场景里,可能restful 接口返回最多的类型是 json,那如果是 json,我们的测试用例又该如何改造呢,我们直接在下面把代码贴出来。
我们还是老样子,改造接口返回类型,然后在ETest.java
加上一个测试方法。
1 2 3 4 5 6 @GetMapping("/test") public String test () { System.out.println("this is test" ); return "{\"msg\":\"success\"}" ; }
1 2 3 4 5 6 7 8 9 10 11 12 @Test @DisplayName("测试模拟请求的Json响应体") void testContentJSon (@Autowired MockMvc mvc) throws Exception { RequestBuilder builder = MockMvcRequestBuilders.get("/user/test" ); ResultActions actions = mvc.perform(builder); ContentResultMatchers content = MockMvcResultMatchers.content(); ResultMatcher matcher = content.json("{\"msg\":\"success\"}" ); actions.andExpect(matcher); }
我们可以看到,这个代码是大差不差的,所以这里我们也不再展开来说明了。下面,我们继续来说说匹配响应头的方法~
模块七:模拟请求-匹配响应头 我们假设我们的场景是要验证下'/user/test'
接口的响应头Content-Type
这个参数的值,那我们就在ETest.java
加上一个测试方法。
1 2 3 4 5 6 7 8 9 10 11 12 @Test @DisplayName("测试模拟请求的响应头") void testContentType (@Autowired MockMvc mvc) throws Exception { RequestBuilder builder = MockMvcRequestBuilders.get("/user/test" ); ResultActions actions = mvc.perform(builder); HeaderResultMatchers header = MockMvcResultMatchers.header(); ResultMatcher matcher = header.string("Content-Type" , "application/json" ); actions.andExpect(matcher); }
此时,我们运行的时候,就可以看到比对的结果了。(这里就不贴出来了)
模块七:业务层测试数据回滚 众所周知,我们在执行测试用例的时候,假设是一些数据增删改的操作,那无非会给我们的数据库留下一组又一组没有用的测试数据,这样直接污染了我们的数据内容,那么我们是不是可以通过SpringBoot提供的测试组件来解决这个问题呢?
下面,我们就来说说遇到这样的情况,我们又该如何进行测试用例的编辑和使用。
首先,新建一个测试类FTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @SpringBootTest @DisplayName("F功能测试类") @Transactional @Rollback public class FTest { @Autowired private TblUserService userService; @Test @DisplayName("测试保存用户") void testSave () { TblUser user = new TblUser (); user.setName("撞飞" ); user.setAge(35 ); user.setRemarks("力拔山兮气盖世" ); userService.save(user); } }
这样子的话,我们就可以实现返回事务回滚,从而不会使测试后的脏数据影响到数据库了。
模块八:测试用例配置随机数据 在我们上面的测试用例中,当我们要进行一些保存的测试时,需要我们去造数据,这得是一个多么累的活呀,能不能像前端的mockjs
一样自动生成随机数据呢,显然,这也是可以实现的,那下面我们就来说说怎么配置随机数据用于测试用例。
首先,我们现在application.yml
配置我们的随机数据
1 2 3 4 5 6 testcase: user: id: ${random.uuid} name: 测试-${random.value} age: ${random.int(1,30)} remarks: ${random.long}
以上的配置我们就举了几个常见的类型作为示例。
其次,我们需要将这些数据交给 springboot 管理,我们创建一个实体类,来加载这些配置数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Data @Component @ConfigurationProperties(prefix = "testcase.user") public class UserCase { private String id; private String name; private Integer age; private Long remarks; }
最后,我们就可以新建一个测试类GTest.java
,多次运行来查看我们的数据是否是随机的
1 2 3 4 5 6 7 8 9 10 11 12 13 @SpringBootTest @DisplayName("G功能测试类") public class GTest { @Autowired private UserCase userCase; @Test @DisplayName("测试通过配置生成随机数据") void test () { System.out.println("userCase = " + userCase); } }
三次运行结果如下:
1 2 3 4 5 6 # 第一次 userCase = UserCase(id=1a4eeedd-13d5-4b7a-a5d9-c563f8d17728, name=测试-ef22391498551eaf5432cb5990f42c9f, age=23, remarks=8470340192971314075)# 第二次 userCase = UserCase(id=94f85687-3d6f-4f0b-b324-33df647cdc87, name=测试-bf3ac1b46c7d5944bceeed14eb47c14f, age=17, remarks=-8987874862682056374)# 第三次 userCase = UserCase(id=7dae0df4-2429-4dbe-a431-587e85cccffe, name=测试-7d52ac988e832b5099ae9a420175722e, age=5, remarks=3053358157117412412)
总结 最后,我想说其实测试用例是很多用法的,以上我们只是介绍了一些简单的使用方法,具体的使用还是需要各位同学们去实践的,毕竟实现是检验真理的唯一标准。今天的内容就到这啦~