springboot2测试用例怎么玩

作为后端开发,我们很容易就忽略了测试用例的重要性,就好像我们容易忽略注释一样,这些我们看似并不重要的东西,其实贯穿于我们整个功能的健壮性以及后续的维护,所以今天我们就来讲讲 springboot 上的测试用例怎么写,怎么用。

前言

在进行下面测试用例的学习前,我们需要先准备一个 springboot 工程。这边我们从https://start.spring.io/直接搞一个

image-20230911144823879

快速搞个 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组件

  1. pom.xml中添加依赖
1
2
3
4
5
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
  1. 逆向工程生成对应的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
//springboot 给我们提供的专用测试用例注解  properties可以帮我们临时引入专用的测试属性
@SpringBootTest(properties = {"a.isOpen=true"})
// 描述我们这个测试类的名称 junit5 才提供
@DisplayName("A功能测试类")
public class ATest {
@Value("${a.isOpen}")
private boolean isOpen;

// junit 提供的测试注解
@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
//springboot 给我们提供的专用测试用例注解  args可以帮我们临时引入专用的命令行参数
@SpringBootTest(args = {"--server.port=9999"})
// 描述我们这个测试类的名称 junit5 才提供
@DisplayName("B 功能测试类")
public class BTest {
@Value("${server.port}")
private int port;

// junit 提供的测试注解
@Test
// 描述我们这个测试方法的名称
@DisplayName("测试 b 功能获取传入参数")
void testB() {
System.out.println("port = " + port);
}
}

模块二:加载测试专用配置

基于上面的加载专用属性的场景下,如果我们要加载更多的自定义的测试属性,那我们又该如何去做呢?

  1. test目录下,新建一个配置类MsgConfig.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 加上配置注解,交给 spring 管理
    @Configuration
    public class MsgConfig {
    // 声明一个 bean
    @Bean
    public String msg() {
    return "hello world";
    }
    }
  2. 新建一个测试类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 环境。

image-20230911221304899

我们可以从以上SpringBootTest源码中看出,确实除了指定获取参数,我们还可以模拟出一个 web 环境,具体我们用代码来操作一下。

下面,我们写一个测试类Dtest.java,然后我们启动它来看一下控制台的输出

1
2
3
4
5
6
7
8
9
// 启动一个 web 环境,用自定义的接口(即server.port)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@DisplayName("D功能测试类")
public class DTest {
@Test
void test() {
System.out.println("true = " + true);
}
}

image-20230911221611362

我们可以看出确实是启动了一个 web 环境,当然除了使用我们自定义端口外,它还提供了以下几种

  1. SpringBootTest.WebEnvironment.RANDOM_PORT 以随机端口号启动
  2. SpringBootTest.WebEnvironment.MOCK 提供一个 mock的 servlet 环境,内置的servlet 容器并没有启动,主要是搭配@AutoConfigureMockMvc
  3. 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)
// 开启虚拟 mvc 调用
@AutoConfigureMockMvc
@DisplayName("E功能测试类")
public class ETest {
@Test
@DisplayName("测试模拟请求")
void test(@Autowired MockMvc mvc) throws Exception {
// 模拟 get 请求,访问/user/test
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
// 控制层 UserController  返回类型可以改造成一个对象User(我这边就偷懒啦~)
@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
// 有上面的事务注解就够了 下面这个回滚的注解默认是 true 可以不加
@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)

总结

最后,我想说其实测试用例是很多用法的,以上我们只是介绍了一些简单的使用方法,具体的使用还是需要各位同学们去实践的,毕竟实现是检验真理的唯一标准。今天的内容就到这啦~


springboot2测试用例怎么玩
https://gcoder5.com/2023/09/11/springboot2测试用例怎么玩/
作者
Gcoder
发布于
2023年9月11日
许可协议