写给想入门单元测试的你
✨这里是第七人格的博客✨小七,欢迎您的到来~✨
🍅系列专栏:【架构思想】🍅
✈️本篇内容: 写给想入门单元测试的你✈️
🍱本篇收录完整代码地址:gitee.com/diqirenge/s…🍱
一、为什么要进行单元测试
首先我们来看一下标准的软件开发流程是什么样的
从图中我们可以看到,单元测试作为开发流程中的重要一环,其实是保证代码健壮性的重要一环,但是因为各种各样的原因,在日常开发中,我们往往不重视这一步,不写或者写的不太规范。那为什么要进行单元测试呢?小七觉得有以下几点:
- 便于后期重构。单元测试可以为代码的重构提供保障,只要重构代码之后单元测试全部运行通过,那么在很大程度上表示这次重构没有引入新的BUG,当然这是建立在完整、有效的单元测试覆盖率的基础上。
- 优化设计。编写单元测试将使用户从调用者的角度观察、思考,特别是使用TDD驱动开发的开发方式,会让使用者把程序设计成易于调用和可测试,并且解除软件中的耦合。
- 文档记录。单元测试就是一种无价的文档,它是展示函数或类如何使用的最佳文档,这份文档是可编译、可运行的、并且它保持最新,永远与代码同步。
- 具有回归性。自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地地快速运行测试,而不是将代码部署到设备之后,然后再手动地覆盖各种执行路径,这样的行为效率低下,浪费时间。
不少同学,写单元测试,就是直接调用的接口方法,就跟跑swagger和postMan一样,这样只是对当前方法有无错误做了一个验证,无法构成单元测试网络。
比如下面这种代码
@Test
public void Test1(){
xxxService.doSomeThing();
}
接下来小七就和大家探讨一下如何写好一个简单的单元测试。
小七觉得写好一个单元测试应该要注意以下几点:
1、单元测试是主要是关注测试方法的逻辑,而不仅仅是结果。
2、需要测试的方法,不应该依赖于其他的方法,也就是说每一个单元各自独立。
3、无论执行多少次,其结果是一定的不变的,也就是单元测试需要有幂等性。
4、单元测试也应该迭代维护。
二、单元测试需要引用的jar包
针对springboot项目,咱们只需要引用他的starter即可
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
下面贴出这个start包含的依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starters</artifactId>
<version>2.1.0.RELEASE</version>
</parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.1.0.RELEASE</version>
<name>Spring Boot Test Starter</name>
<description>Starter for testing Spring Boot applications with libraries including
JUnit, Hamcrest and Mockito</description>
<url>https://projects.spring.io/spring-boot/#/spring-boot-parent/spring-boot-starters/spring-boot-starter-test</url>
<organization>
<name>Pivotal Software, Inc.</name>
<url>https://spring.io</url>
</organization>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0</url>
</license>
</licenses>
<developers>
<developer>
<name>Pivotal</name>
<email>info@pivotal.io</email>
<organization>Pivotal Software, Inc.</organization>
<organizationUrl>http://www.spring.io</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/spring-projects/spring-boot.git/spring-boot-starters/spring-boot-starter-test</connection>
<developerConnection>scm:git:ssh://git@github.com/spring-projects/spring-boot.git/spring-boot-starters/spring-boot-starter-test</developerConnection>
<url>http://github.com/spring-projects/spring-boot/spring-boot-starters/spring-boot-starter-test</url>
</scm>
<issueManagement>
<system>Github</system>
<url>https://github.com/spring-projects/spring-boot/issues</url>
</issueManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.1.0.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<version>2.1.0.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test-autoconfigure</artifactId>
<version>2.1.0.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.4.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.11.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.23.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>1.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<version>1.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.skyscreamer</groupId>
<artifactId>jsonassert</artifactId>
<version>1.5.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.1.2.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.2.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.xmlunit</groupId>
<artifactId>xmlunit-core</artifactId>
<version>2.6.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
三、单元测试解析与技巧
1、单元测试类注解解析
下面是出现频率极高的注解:
/*
* 这个注解的作用是,在执行单元测试的时候,不是直接去执行里面的单元测试的方法
* 因为那些方法执行之前,是需要做一些准备工作的,它是需要先初始化一个spring容器的
* 所以得找这个SpringRunner这个类,来先准备好spring容器,再执行各个测试方法
*/
@RunWith(SpringRunner.class)
/*
* 这个注解的作用是,去寻找一个标注了@SpringBootApplication注解的一个类,也就是启动类
* 然后会执行这个启动类的main方法,就可以创建spring容器,给后面的单元测试提供完整的这个环境
*/
@SpringBootTest
/*
* 这个注解的作用是,可以让每个方法都是放在一个事务里面
* 让单元测试方法执行的这些增删改的操作,都是一次性的
*/
@Transactional
/*
* 这个注解的作用是,如果产生异常那么会回滚,保证数据库数据的纯净
* 默认就是true
*/
@Rollback(true)
2、常用断言
Junit所有的断言都包含在 Assert 类中。
void assertEquals(boolean expected, boolean actual) | 检查两个变量或者等式是否平衡 |
---|---|
void assertTrue(boolean expected, boolean actual) | 检查条件为真 |
void assertFalse(boolean condition) | 检查条件为假 |
void assertNotNull(Object object) | 检查对象不为空 |
void assertNull(Object object) | 检查对象为空 |
void assertArrayEquals(expectedArray, resultArray) | 检查两个数组是否相等 |
void assertSame(expected, actual) | 查看两个对象的引用是否相等。类似于使用“==”比较两个对象 |
assertNotSame(unexpected, actual) | 查看两个对象的引用是否不相等。类似于使用“!=”比较两个对象 |
fail() | 让测试失败 |
static T verify(T mock, VerificationMode mode) | 验证调用次数,一般用于void方法 |
3、有返回值方法的测试
@Test
public void haveReturn() {
// 1、初始化数据
// 2、模拟行为
// 3、调用方法
// 4、断言
}
4、无返回值方法的测试
@Test
public void noReturn() {
// 1、初始化数据
// 2、模拟行为
// 3、调用方法
// 4、验证执行次数
}
四、单元测试小例
以常见的SpringMVC3层架构为例,咱们分别展示3层架构如何做简单的单元测试。业务场景为用户user的增删改查。
(1)dao层的单元测试
dao层一般是持久化层,也就是与数据库打交道的一层,单元测试尽量不要依赖外部,但是直到最后一层的时候,DAO层的时候,还是要依靠开发环境里的基础设施,来进行单元测试。
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
@Rollback
public class UserMapperTest {
/**
* 持久层,不需要使用模拟对象
*/
@Autowired
private UserMapper userMapper;
/**
* 测试用例:查询所有用户信息
*/
@Test
public void testListUsers() {
// 初始化数据
initUser(20);
// 调用方法
List<User> resultUsers = userMapper.listUsers();
// 断言不为空
assertNotNull(resultUsers);
// 断言size大于0
Assert.assertThat(resultUsers.size(), is(greaterThanOrEqualTo(0)));
}
/**
* 测试用例:根据ID查询一个用户
*/
@Test
public void testGetUserById() {
// 初始化数据
User user = initUser(20);
Long userId = user.getId();
// 调用方法
User resultUser = userMapper.getUserById(userId);
// 断言对象相等
assertEquals(user.toString(), resultUser.toString());
}
/**
* 测试用例:新增用户
*/
@Test
public void testSaveUser() {
initUser(20);
}
/**
* 测试用例:修改用户
*/
@Test
public void testUpdateUser() {
// 初始化数据
Integer oldAge = 20;
Integer newAge = 21;
User user = initUser(oldAge);
user.setAge(newAge);
// 调用方法
Boolean updateResult = userMapper.updateUser(user);
// 断言是否为真
assertTrue(updateResult);
// 调用方法
User updatedUser = userMapper.getUserById(user.getId());
// 断言是否相等
assertEquals(newAge, updatedUser.getAge());
}
/**
* 测试用例:删除用户
*/
@Test
public void testRemoveUser() {
// 初始化数据
User user = initUser(20);
// 调用方法
Boolean removeResult = userMapper.removeUser(user.getId());
// 断言是否为真
assertTrue(removeResult);
}
private User initUser(int i) {
// 初始化数据
User user = new User();
user.setName("测试用户");
user.setAge(i);
// 调用方法
userMapper.saveUser(user);
// 断言id不为空
assertNotNull(user.getId());
return user;
}
}
(2)service层的单元测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceImplTest {
@Autowired
private UserService userService;
/**
* 这个注解表名,该对象是个mock对象,他将替换掉你@Autowired标记的对象
*/
@MockBean
private UserMapper userMapper;
/**
* 测试用例:查询所有用户信息
*/
@Test
public void testListUsers() {
// 初始化数据
List<User> users = new ArrayList<>();
User user = initUser(1L);
users.add(user);
// mock行为
when(userMapper.listUsers()).thenReturn(users);
// 调用方法
List<User> resultUsers = userService.listUsers();
// 断言是否相等
assertEquals(users, resultUsers);
}
/**
* 测试用例:根据ID查询一个用户
*/
@Test
public void testGetUserById() {
// 初始化数据
Long userId = 1L;
User user = initUser(userId);
// mock行为
when(userMapper.getUserById(userId)).thenReturn(user);
// 调用方法
User resultUser = userService.getUserById(userId);
// 断言是否相等
assertEquals(user, resultUser);
}
/**
* 测试用例:新增用户
*/
@Test
public void testSaveUser() {
// 初始化数据
User user = initUser(1L);
// 默认的行为(这一行可以不写)
doNothing().when(userMapper).saveUser(any());
// 调用方法
userService.saveUser(user);
// 验证执行次数
verify(userMapper, times(1)).saveUser(user);
}
/**
* 测试用例:修改用户
*/
@Test
public void testUpdateUser() {
// 初始化数据
User user = initUser(1L);
// 模拟行为
when(userMapper.updateUser(user)).thenReturn(true);
// 调用方法
Boolean updateResult = userService.updateUser(user);
// 断言是否为真
assertTrue(updateResult);
}
/**
* 测试用例:删除用户
*/
@Test
public void testRemoveUser() {
Long userId = 1L;
// 模拟行为
when(userMapper.removeUser(userId)).thenReturn(true);
// 调用方法
Boolean removeResult = userService.removeUser(userId);
// 断言是否为真
assertTrue(removeResult);
}
private User initUser(Long userId) {
User user = new User();
user.setName("测试用户");
user.setAge(20);
user.setId(userId);
return user;
}
}
(3)controller层的单元测试
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class UserControllerTest {
private MockMvc mockMvc;
@InjectMocks
private UserController userController;
@MockBean
private UserService userService;
/**
* 前置方法,一般执行初始化代码
*/
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
this.mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
}
/**
* 测试用例:查询所有用户信息
*/
@Test
public void testListUsers() {
try {
List<User> users = new ArrayList<User>();
User user = new User();
user.setId(1L);
user.setName("测试用户");
user.setAge(20);
users.add(user);
when(userService.listUsers()).thenReturn(users);
mockMvc.perform(get("/user/"))
.andExpect(content().json(JSONArray.toJSONString(users)));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 测试用例:根据ID查询一个用户
*/
@Test
public void testGetUserById() {
try {
Long userId = 1L;
User user = new User();
user.setId(userId);
user.setName("测试用户");
user.setAge(20);
when(userService.getUserById(userId)).thenReturn(user);
mockMvc.perform(get("/user/{id}", userId))
.andExpect(content().json(JSONObject.toJSONString(user)));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 测试用例:新增用户
*/
@Test
public void testSaveUser() {
Long userId = 1L;
User user = new User();
user.setName("测试用户");
user.setAge(20);
when(userService.saveUser(user)).thenReturn(userId);
try {
mockMvc.perform(post("/user/").contentType("application/json").content(JSONObject.toJSONString(user)))
.andExpect(content().string("success"));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 测试用例:修改用户
*/
@Test
public void testUpdateUser() {
Long userId = 1L;
User user = new User();
user.setId(userId);
user.setName("测试用户");
user.setAge(20);
when(userService.updateUser(user)).thenReturn(true);
try {
mockMvc.perform(put("/user/{id}", userId).contentType("application/json").content(JSONObject.toJSONString(user)))
.andExpect(content().string("success"));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 测试用例:删除用户
*/
@Test
public void testRemoveUser() {
Long userId = 1L;
when(userService.removeUser(userId)).thenReturn(true);
try {
mockMvc.perform(delete("/user/{id}", userId))
.andExpect(content().string("success"));
} catch (Exception e) {
e.printStackTrace();
}
}
}
五、其他
1、小七认为不需要对私有方法进行单元测试。
2、dubbo的接口,在初始化的时候会被dubbo的类代理,和单测的mock是两个类,会导致mock失效,目前还没有找到好的解决方案。
3、单元测试覆盖率报告
(1)添加依赖
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.2</version>
</dependency>
(2)添加插件
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.2</version>
<executions>
<execution>
<id>pre-test</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>post-test</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
(3)执行mvn test命令
报告生成位置
4、异常测试
本次分享主要是针对正向流程,异常情况未做处理。感兴趣的同学可以查看附录相关文档自己学习。
六、附录
1、user建表语句:
CREATE TABLE `user` (
`id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
`name` VARCHAR(32) NOT NULL UNIQUE COMMENT '用户名',
`age` INT(3) NOT NULL COMMENT '年龄'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='user示例表';
2、文章小例源码地址:gitee.com/diqirenge/s…
3、mockito官网:site.mockito.org/
4、mockito中文文档:github.com/hehonghui/m…
来源:juejin.cn/post/7297608084306821132