SpringBoot编写单元测试
引入单元测试
Spring Boot中引入单元测试很简单,依赖如下:
1
2
3
4
5<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Service层单元测试
Spring Boot中单元测试类写在在src/test/java目录下,你可以手动创建具体测试类,
如果是IDEA,则可以通过IDEA自动创建测试类(Navigate -> Test),也可以通过快捷键
⇧⌘T
(MAC)或者Ctrl+Shift+T
(Window)来创建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
38package tk.fulsun.demo.service;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
/**
* @Description Service层单元测试
* @Date 2021/6/13
* @Created by 凉月-文
*/
class ServiceATest {
private ServiceA serviceA;
void setUp() {
System.out.println("start=============");
}
void tearDown() {
System.out.println("end=============");
}
public void test1() {
serviceA.doSomething();
System.out.println("--------------测试----------");
}
}上面就是最简单的单元测试写法,顶部只要
@RunWith(SpringRunner.class)
和SpringBootTest
即可,想要执行的时候,鼠标放在对应的方法,右键选择run该方法即可。
Controller层单元测试
- 上面只是针对Service层做测试,但是有时候需要对Controller层(API)做测试,这时候就得用到MockMvc了,你可以不必启动工程就能测试这些接口。
- MockMvc实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,这样可以使得测试速度快、不依赖网络环境,而且提供了一套验证的工具,这样可以使得请求的验证统一而且很方便。
Controller类
用户的controller类如下
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62package tk.fulsun.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import tk.fulsun.demo.mapper.UserMapper;
import tk.fulsun.demo.model.User;
import tk.fulsun.demo.model.UserExample;
import java.util.List;
/**
* @Description ControllerA
* @Date 2021/6/13
* @Created by 凉月-文
*/
public class ControllerA {
private UserMapper userMapper;
public String test() {
return "test-ControllerA";
}
public List<User> getAllUser() {
return userMapper.selectByExample(null);
}
public List<User> getUserById( { String name)
UserExample example = new UserExample();
UserExample.Criteria criteria = example.createCriteria();
criteria.andNameLike('%' + name + '%');
return userMapper.selectByExample(example);
}
public String addUser( { User user)
int status = userMapper.insertSelective(user);
return status > 0 ? "成功" : "失败";
}
public String updateUser( { User user)
int status = userMapper.updateByPrimaryKeySelective(user);
return status > 0 ? "成功" : "失败";
}
public String delUser(int id) {
int status = userMapper.deleteByPrimaryKey(id);
return status > 0 ? "成功" : "失败";
}
}
测试类
这里我们也自动创建一个Controller的测试类,具体代码如下
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101import com.alibaba.fastjson.JSON;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import tk.fulsun.demo.model.User;
import static org.junit.jupiter.api.Assertions.*;
/**
* @Description ControllerATest
* @Date 2021/6/13
* @Created by 凉月-文
*/
class ControllerATest {
private WebApplicationContext wac;
private MockMvc mvc;
private MockHttpSession session;
public void setupMockMvc() {
mvc = MockMvcBuilders.webAppContextSetup(wac).build(); //初始化MockMvc对象
session = new MockHttpSession();
User user = new User("root", 666);
session.setAttribute("user", user); //拦截器那边会判断用户是否登录,所以这里注入一个用户
}
void getAllUser() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/test/user/all")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.session(session))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print());
}
void getUserById() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/test/user/1")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.session(session))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("张三"))
.andExpect(MockMvcResultMatchers.jsonPath("$.age").value(23))
.andDo(MockMvcResultHandlers.print());
}
void addUser() throws Exception {
User user = new User("凉月文", 23);
String json = JSON.toJSONString(user);
mvc.perform(MockMvcRequestBuilders.post("/test/user/add")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.content(json.getBytes())
.session(session))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print());
}
void updateUser() throws Exception {
User user = new User("凉月文", 22);
user.setId(3);
String json = JSON.toJSONString(user);
mvc.perform(MockMvcRequestBuilders.put("/test/user/update")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.content(json.getBytes())
.session(session))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print());
}
void delUser() throws Exception {
mvc.perform(MockMvcRequestBuilders.delete("/test/user/3")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.session(session))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print());
}
}
MockMvc简单的方法
用下面的代码解释下用法
1
2
3
4
5
6
7
8
9
10
11
void getUserById() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/test/user/1")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.session(session))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.name").value("张三"))
.andExpect(MockMvcResultMatchers.jsonPath("$.age").value(23))
.andDo(MockMvcResultHandlers.print());
}mockMvc.perform
执行一个请求MockMvcRequestBuilders.get"/test/user/1")
构造一个请求,Post请求就用.post方法contentType(MediaType.APPLICATION_JSON_UTF8)
代表发送端发送的数据格式是application/json;charset=UTF-8
accept(MediaType.APPLICATION_JSON_UTF8)
代表客户端希望接受的数据类型为application/json;charset=UTF-8
session(session)
注入一个session,这样拦截器才可以通过ResultActions.andExpect
添加执行完成后的断言ResultActions.andExpect(MockMvcResultMatchers.status().isOk())
方法看请求的状态响应码是否为200如果不是则抛异常,测试不通过andExpect(MockMvcResultMatchers.jsonPath(“$.name”).value("张三"))
这里jsonPath用来获取author字段比对是否为张三
,不是就测试不通过ResultActions.andDo
添加一个结果处理器,表示要对结果做点什么事情,比如此处使用MockMvcResultHandlers.print()
输出整个响应结果信息
注解方式创建
@AutoConfigureMockMvc //相当于new MockMvc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
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.status;
class MockMvcExampleTests {
void exampleTest( MockMvc mvc)throws Exception {
mvc.perform(get("/")).andExpect(status().isOk()).andExpect(content().string("Hello World"));
}
}
断言AssertThat
- JUnit 4.4 结合 Hamcrest 提供了一个全新的断言语法——assertThat。
- 程序员可以只使用 assertThat 一个断言语句,结合 Hamcrest 提供的匹配符,就可以表达全部的测试思想。
基本语法
1 | assertThat( [value], [matcher statement] ); |
- value 是接下来想要测试的变量值;
- matcher statement 是使用 Hamcrest 匹配符来表达的对前面变量所期望的值的声明,如果 value 值与 matcher statement 所表达的期望值相符,则测试成功,否则测试失败。
优点
优点 1:以前 JUnit 提供了很多的 assertion 语句,如:assertEquals,assertNotSame,assertFalse,assertTrue,assertNotNull,assertNull 等,现在有了 JUnit 4.4,一条 assertThat 即可以替代所有的 assertion 语句,这样可以在所有的单元测试中只使用一个断言方法,使得编写测试用例变得简单,代码风格变得统一,测试代码也更容易维护。
优点 2:assertThat 使用了 Hamcrest 的 Matcher 匹配符,用户可以使用匹配符规定的匹配准则精确的指定一些想设定满足的条件,具有很强的易读性,而且使用起来更加灵活。
使用匹配符 Matcher 和不使用之间的比较
1
2
3
4
5// 想判断某个字符串 s 是否含有子字符串 "developer" 或 "Works" 中间的一个
// JUnit 4.4 以前的版本:assertTrue(s.indexOf("developer")>-1||s.indexOf("Works")>-1 );
// JUnit 4.4:
assertThat(s, anyOf(containsString("developer"), containsString("Works")));
// 匹配符 anyOf 表示任何一个条件满足则成立,类似于逻辑或 "||", 匹配符 containsString 表示是否含有参数子
优点 3:assertThat 不再像 assertEquals 那样,使用比较难懂的“谓宾主”语法模式(如:assertEquals(3, x);),相反,assertThat 使用了类似于“主谓宾”的易读语法模式(如:assertThat(x,is(3));),使得代码更加直观、易读。
优点 4:可以将这些 Matcher 匹配符联合起来灵活使用,达到更多目的
1
2
3
4
5
6// 联合匹配符not和equalTo表示“不等于”
assertThat( something, not( equalTo( "developer" ) ) );
// 联合匹配符not和containsString表示“不包含子字符串”
assertThat( something, not( containsString( "Works" ) ) );
// 联合匹配符anyOf和containsString表示“包含任何一个子字符串”
assertThat(something, anyOf(containsString("developer"), containsString("Works")))优点 5:错误信息更加易懂、可读且具有描述性(descriptive)
JUnit 4.4 以前的版本默认出错后不会抛出额外提示信息,如:
1
assertTrue( s.indexOf("developer") > -1 || s.indexOf("Works") > -1 );
如果该断言出错,只会抛出无用的错误信息,如:junit.framework.AssertionFailedError:null。
如果想在出错时想打印出一些有用的提示信息,必须得程序员另外手动写,如:
1
2assertTrue( "Expected a string containing 'developer' or 'Works'",
s.indexOf("developer") > -1 || s.indexOf("Works") > -1 );JUnit 4.4 会默认自动提供一些可读的描述信息,如
1
2
3
4
5
6String s = "hello world!";
assertThat( s, anyOf( containsString("developer"), containsString("Works") ) );
// 如果出错后,系统会自动抛出以下提示信息:
java.lang.AssertionError:
Expected: (a string containing "developer" or a string containing "Works")
got: "hello world!"
使用 assertThat
JUnit 4.4 自带了一些 Hamcrest 的匹配符 Matcher,但是只有有限的几个,在类 org.hamcrest.CoreMatchers 中定义,要想使用他们,必须导入包 org.hamcrest.CoreMatchers.*。
大部分 assertThat 的使用例子:
字符相关匹配符
1 | /**equalTo匹配符断言被测的testedValue等于expectedValue, |
一般匹配符
1 | /**nullValue()匹配符断言被测object的值为null*/ |
数值相关匹配符
1 | /**closeTo匹配符断言被测的浮点型数testedDouble在20.0±0.5范围之内*/ |
集合相关匹配符
1 | /**hasEntry匹配符断言被测的Map对象mapObject含有一个键值为"key"对应元素值为"value"的Entry项*/ |
单元测试的回滚
单元个测试的时候如果不想造成垃圾数据,可以开启事物功能,记在方法或者类头部添加
@Transactional
注解即可,如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void addUser() throws Exception {
User user = new User("凉月文", 23);
String json = JSON.toJSONString(user);
mvc.perform(MockMvcRequestBuilders.post("/test/user/add")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.content(json.getBytes())
.session(session))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print());
}
// log
2021-06-13 11:55:40.409 INFO 12340 --- [ main] o.s.t.c.transaction.TransactionContext : Rolled back transaction for test:这样测试完数据就会回滚了,不会造成垃圾数据。如果你想关闭回滚,只要加上
@Rollback(false)
注解即可。@Rollback
表示事务执行完回滚,支持传入一个参数value,默认true即回滚,false不回滚。
mysql的存储引擎
如果你使用的数据库是Mysql,有时候会发现加了注解
@Transactional
也不会回滚,那么你就要查看一下你的默认引擎是不是InnoDB,如果不是就要改成InnoDB。MyISAM与InnoDB是mysql目前比较常用的两个数据库存储引擎,MyISAM与InnoDB的主要的不同点在于性能和事务控制上。这里简单的介绍一下两者间的区别和转换方法:
MyISAM:MyISAM是MySQL5.5之前版本默认的数据库存储引擎。MYISAM提供高速存储和检索,以及全文搜索能力,适合数据仓库等查询频繁的应用。但不支持事务、也不支持外键。MyISAM格式的一个重要缺陷就是不能在表损坏后恢复数据。
InnoDB:InnoDB是MySQL5.5版本的默认数据库存储引擎,不过InnoDB已被Oracle收购,MySQL自行开发的新存储引擎Falcon将在MySQL6.0版本引进。InnoDB具有提交、回滚和崩溃恢复能力的事务安全。但是比起MyISAM存储引擎,InnoDB写的处理效率差一些并且会占用更多的磁盘空间以保留数据和索引。尽管如此,但是InnoDB包括了对事务处理和外来键的支持,这两点都是MyISAM引擎所没有的。
MyISAM适合:(1)做很多count 的计算;(2)插入不频繁,查询非常频繁;(3)没有事务。
InnoDB适合:(1)可靠性要求比较高,或者要求事务;(2)表更新和查询都相当的频繁,并且表锁定的机会比较大的情况。(4)性能较好的服务器,比如单独的数据库服务器,像阿里云的关系型数据库RDS就推荐使用InnoDB引擎。
修改默认引擎的步骤
查看MySQL当前默认的存储引擎:
1
mysql> show variables like '%storage_engine%';
你要看user表用了什么引擎(在显示结果里参数engine后面的就表示该表当前用的存储引擎):
1
mysql> show create table user;
将user表修为InnoDB存储引擎(也可以此命令将InnoDB换为MyISAM):
1
mysql> ALTER TABLE user ENGINE=INNODB;
如果要更改整个数据库表的存储引擎,一般要一个表一个表的修改,比较繁琐,可以采用先把数据库导出,得到SQL,把MyISAM全部替换为INNODB,再导入数据库的方式。转换完毕后重启mysql
Mock 测试
- Mock 测试就是在测试过程中,创建一个假的对象,避免你为了测试一个方法,却要自行构建整个 Bean 的依赖链。
Mockito
Mockito 是一种 Java Mock 框架,主要就是用来做 Mock 测试的,它可以模拟任何 Spring 管理的 Bean、模拟方法的返回值、模拟抛出异常等等,同时也会记录调用这些模拟方法的参数、调用顺序,从而可以校验出这个 Mock 对象是否有被正确的顺序调用,以及按照期望的参数被调用。
像是 Mockito 可以在单元测试中模拟一个 Service 返回的数据,而不会真正去调用该 Service,通过模拟一个假的 Service 对象,来快速的测试当前想要测试的类。
目前在 Java 中主流的 Mock 测试工具有 Mockito、JMock、EasyMock等等,而 SpringBoot 目前默认的测试框架是 Mockito 框架。
使用 Mockito
test依赖中包含了mockito的依赖
1
2
3
4
5<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
service编写
在service中通过Mapper层查询数据库操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ServiceA {
public void doSomething() {
System.out.println("ServiceA running .... ");
}
private UserMapper userDao;
public User getUserById(Integer id) {
return userDao.selectByPrimaryKey(id);
}
public Integer insertUser(User user) {
return userDao.insert(user);
}
}
实际测试
测试代码如下
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
41
42package tk.fulsun.demo.controller;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import tk.fulsun.demo.mapper.UserMapper;
import tk.fulsun.demo.model.User;
import tk.fulsun.demo.service.ServiceA;
import java.util.List;
/**
* @Description Mock测试
* @Date 2021/6/13
* @Created by 凉月-文
*/
public class ControllerAMockTest {
private ServiceA serviceA ;
public void getUserById() throws Exception {
//普通的使用userService,他里面会再去调用userDao取得数据库的数据
User user = serviceA.getUserById(1);
System.out.println(user);
//检查结果
Assert.assertNotNull(user);
Assert.assertEquals(user.getId(), new Integer(1));
Assert.assertEquals(user.getName(), "凉月文");
}
}测试结果
模拟UserDao
如果 userDao 还没写好,又想先测 userService 的话,就需要使用 Mockito 去模拟一个假的 userDao 出来。
在 userDao 上加上一个
@MockBean
注解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
41
42
43
44
45
46
47
48package tk.fulsun.demo.controller;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import tk.fulsun.demo.mapper.UserMapper;
import tk.fulsun.demo.model.User;
import tk.fulsun.demo.service.ServiceA;
import java.util.List;
/**
* @Description Mock测试
* @Date 2021/6/13
* @Created by 凉月-文
*/
public class ControllerAMockTest {
private ServiceA serviceA ;
private UserMapper userDao;
public void getUserById() throws Exception {
// 定义当调用mock userDao的getUserById()方法,并且参数为3时,就返回id为200、name为I'm mock3的user对象
Mockito.when(userDao.selectByPrimaryKey(1)).thenReturn(new User("凉月文", 18));
//普通的使用userService,他里面会再去调用userDao取得数据库的数据
//返回的会是定义的 user("凉月文", 18)对象
User user = serviceA.getUserById(1);
System.out.println(user);
//检查结果
Assert.assertNotNull(user);
Assert.assertEquals(user.getId(), new Integer(1));
Assert.assertEquals(user.getName(), "凉月文");
}
}测试结果可以看到输出的user对象就是定义的mock对象
Mockito方法
- 最基本的
Mockito.when( 对象.方法名() ).thenReturn( 自定义结果 )
,还提供了其他用法让我们使用。
thenReturn
当使用任何整数值调用 userService 的 getUserById() 方法时,就回传一个名字为Aritisan的 User 对象。
1
2
3Mockito.when(userService.getUserById(Mockito.anyInt())).thenReturn(new User(3, "Aritisan"));
User user1 = userService.getUserById(3); // 回传的user的名字为Aritisan
User user2 = userService.getUserById(200); // 回传的user的名字也为Aritisan限制只有当参数的数字是 3 时,才会回传名字为 Aritisan 的 user 对象。
1
2
3Mockito.when(userService.getUserById(3)).thenReturn(new User(3, "Aritisan"));
User user1 = userService.getUserById(3); // 回传的user的名字为Aritisan
User user2 = userService.getUserById(200); // 回传的user为null当调用 userService 的 insertUser() 方法时,不管传进来的 user 是什么,都回传 100。
1
2Mockito.when(userService.insertUser(Mockito.any(User.class))).thenReturn(100);
Integer i = userService.insertUser(new User()); //会返回100
thenThrow
当调用 userService 的 getUserById() 时的参数是 9 时,抛出一个 RuntimeException。
1
2Mockito.when(userService.getUserById(9)).thenThrow(new RuntimeException("mock throw exception"));
User user = userService.getUserById(9); //会抛出一个RuntimeException如果方法没有返回值的话(即是方法定义为 public void myMethod() {…}),要改用 doThrow() 抛出 Exception。
1
2Mockito.doThrow(new RuntimeException("mock throw exception")).when(userService).print();
userService.print(); //会抛出一个RuntimeException
verify
检查调用 userService 的 getUserById()、且参数为3的次数是否为1次。
1
Mockito.verify(userService, Mockito.times(1)).getUserById(Mockito.eq(3)) ;
验证调用顺序,验证 userService 是否先调用 getUserById() 两次,并且第一次的参数是 3、第二次的参数是 5,然后才调用insertUser() 方法。
1
2
3
4InOrder inOrder = Mockito.inOrder(userService);
inOrder.verify(userService).getUserById(3);
inOrder.verify(userService).getUserById(5);
inOrder.verify(userService).insertUser(Mockito.any(User.class));
注意事项
- 不过当使用 Mockito 在 Mock 对象时,有一些限制需要遵守:
- 不能 Mock 静态方法
- 不能 Mock private 方法
- 不能 Mock final class
Test类导入问题
pom文件区别
1
2
3
4
5
6
7
8
9
10
11
12
13
14<!-- Spring Boot 2.2 之后的 pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<!-- junit-vintage-engine 用于运行JUnit 4 引擎测试
junit-jupiter-engine 用于运行JUnit 5 引擎测试-->
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>在代码编写的过程中,Test导包的时候,会有下面二个
- import org.junit.Test (Junit4)
- import org.junit.jupiter.api.Test (Junit5)
spring boot 2.2之前使用的是 Junit4,类用public 修饰
1
2
3
4
5
6
7
8
9
10
11
12
13
14import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
public class Demo1ApplicationTests {
public void contextLoads() {
}
}
If you are using JUnit 4, don’t forget to also add
@RunWith(SpringRunner.class)
to your test, otherwise the annotations will be ignored. If you are using JUnit 5, there’s no need to add the equivalent@ExtendWith(SpringExtension.class)
as@SpringBootTest
and the other@…Test
annotations are already annotated with it.
- spring boot 2.2之后使用的是 Junit5,(不需要@RunWith)
1 | import org.junit.jupiter.api.Test; |
JUnit4 与 JUnit 5对比
常用注解
JUnit4 JUnit5 说明 @Test @Test 表示该方法是一个测试方法。JUnit5与JUnit 4的@Test注解不同的是,它没有声明任何属性,因为JUnit Jupiter中的测试扩展是基于它们自己的专用注解来完成的。这样的方法会被继承,除非它们被覆盖 @BeforeClass @BeforeAll 表示使用了该注解的方法应该在当前类中所有使用了@Test @RepeatedTest、@ParameterizedTest或者@TestFactory注解的方法之前 执行; @AfterClass @AfterAll 表示使用了该注解的方法应该在当前类中所有使用了@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory注解的方法之后执行; @Before @BeforeEach 表示使用了该注解的方法应该在当前类中每一个使用了@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory注解的方法之前 执行 @After @AfterEach 表示使用了该注解的方法应该在当前类中每一个使用了@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory注解的方法之后 执行 @Ignore @Disabled 用于禁用一个测试类或测试方法 @Category @Tag 用于声明过滤测试的tags,该注解可以用在方法或类上;类似于TesgNG的测试组或JUnit 4的分类。 @Parameters @ParameterizedTest 表示该方法是一个参数化测试 @RunWith @ExtendWith @Runwith就是放在测试类名之前,用来确定这个类怎么运行的 @Rule @ExtendWith Rule是一组实现了TestRule接口的共享类,提供了验证、监视TestCase和外部资源管理等能力 @ClassRule @ExtendWith @ClassRule用于测试类中的静态变量,必须是TestRule接口的实例,且访问修饰符必须为public。
Springboot集成JUnit5
为什么使用JUnit5
- JUnit4被广泛使用,但是许多场景下使用起来语法较为繁琐,JUnit5中支持lambda表达式,语法简单且代码不冗余。
- JUnit5易扩展,包容性强,可以接入其他的测试引擎。
- 功能更强大提供了新的断言机制、参数化测试、重复性测试等新功能。
- ps:开发人员为什么还要测试,单测写这么规范有必要吗?其实单测是开发人员必备技能,只不过很多开发人员开发任务太重导致调试完就不管了,没有系统化得单元测试,单元测试在系统重构时能发挥巨大的作用,可以在重构后快速测试新的接口是否与重构前有出入。
JUnit5结构如下
- JUnit Platform: 这是Junit提供的平台功能模块,通过它,其它的测试引擎都可以接入Junit实现接口和执行。
- JUnit JUpiter:这是JUnit5的核心,是一个基于JUnit Platform的引擎实现,它包含许多丰富的新特性来使得自动化测试更加方便和强大。
- JUnit Vintage:这个模块是兼容JUnit3、JUnit4版本的测试引擎,使得旧版本的自动化测试也可以在JUnit5下正常运行。
依赖引入
我们以
SpringBoot2.3.1
为例,引入如下依赖,防止使用旧的junit4相关接口我们将其依赖排除。1
2
3
4
5
6
7
8
9
10
11<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>
常用注解
- @BeforeEach:在每个单元测试方法执行前都执行一遍
- @BeforeAll:在每个单元测试方法执行前执行一遍(只执行一次)
- @DisplayName(“商品入库测试”):用于指定单元测试的名称
- @Disabled:当前单元测试置为无效,即单元测试时跳过该测试
- @RepeatedTest(n):重复性测试,即执行n次
- @ParameterizedTest:参数化测试,
- @ValueSource(ints = {1, 2, 3}):参数化测试提供数据
断言
JUnit Jupiter提供了强大的断言方法用以验证结果,在使用时需要借助java8的新特性lambda表达式,均是来自
org.junit.jupiter.api.Assertions
包的static
方法。assertTrue
与assertFalse
用来判断条件是否为true
或false
1
2
3
4
5
void testEquals() {
assertTrue(3 < 4);
}assertNull
与assertNotNull
用来判断条件是否为null
1
2
3
4
5
void testNotNull() {
assertNotNull(new Object());
}assertThrows
用来判断执行抛出的异常是否符合预期,并可以使用异常类型接收返回值进行其他操作1
2
3
4
5
6
7
8
void testThrows() {
ArithmeticException arithExcep = assertThrows(ArithmeticException.class, () -> {
int m = 5/0;
});
assertEquals("/ by zero", arithExcep.getMessage());
}assertTimeout
用来判断执行过程是否超时1
2
3
4
5
6
7
8
9
void testTimeOut() {
String actualResult = assertTimeout(ofSeconds(2), () -> {
Thread.sleep(1000);
return "a result";
});
System.out.println(actualResult);
}assertAll
是组合断言,当它内部所有断言正确执行完才算通过1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void testAll() {
assertAll("测试item商品下单",
() -> {
//模拟用户余额扣减
assertTrue(1 < 2, "余额不足");
},
() -> {
//模拟item数据库扣减库存
assertTrue(3 < 4);
},
() -> {
//模拟交易流水落库
assertNotNull(new Object());
}
);
}
重复性测试
在许多场景中我们需要对同一个接口方法进行重复测试,例如对幂等性接口的测试。
JUnit Jupiter通过使用
@RepeatedTest(n)
指定需要重复的次数1
2
3
4
5
void repeatedTest() {
System.out.println("调用");
}
参数化测试
参数化测试可以按照多个参数分别运行多次单元测试这里有点类似于重复性测试,只不过每次运行传入的参数不用。
需要使用到
@ParameterizedTest
,同时也需要@ValueSource
提供一组数据,它支持八种基本类型以及String
和自定义对象类型,使用极其方便。1
2
3
4
5
6
void paramTest(int a) {
assertTrue(a > 0 && a < 4);
}
内嵌测试
JUnit5提供了嵌套单元测试的功能,可以更好展示测试类之间的业务逻辑关系,我们通常是一个业务对应一个测试类,有业务关系的类其实可以写在一起。这样有利于进行测试。而且内联的写法可以大大减少不必要的类,精简项目,防止类爆炸等一系列问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MockTest {
//....
class OrderTestClas {
void cancelOrder() {
int status = -1;
System.out.println("取消订单成功,订单状态为:"+status);
}
}
}