从图中我们可以看到,单元测试作为开发流程中的重要一环,其实是保证代码健壮性的重要一环,但是因为各种各样的原因,在日常开发中,我们往往不重视这一步,不写或者写的不太规范。那为什么要进行单元测试呢?小七觉得有以下几点:
不少同学,写单元测试,就是直接调用的接口方法,就跟跑swagger和postMan一样,这样只是对当前方法有无错误做了一个验证,无法构成单元测试网络。
比如下面这种代码
@Test
public void Test1(){
xxxService.doSomeThing();
}
接下来小七就和大家探讨一下如何写好一个简单的单元测试。
小七觉得写好一个单元测试应该要注意以下几点:
1、单元测试是主要是关注测试方法的逻辑,而不仅仅是结果。
2、需要测试的方法,不应该依赖于其他的方法,也就是说每一个单元各自独立。
3、无论执行多少次,其结果是一定的不变的,也就是单元测试需要有幂等性。
4、单元测试也应该迭代维护。
针对springboot项目,咱们只需要引用他的starter即可
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<version>2.1.0.RELEASEversion>
dependency>
下面贴出这个start包含的依赖
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-startersartifactId>
<version>2.1.0.RELEASEversion>
parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<version>2.1.0.RELEASEversion>
<name>Spring Boot Test Startername>
<description>Starter for testing Spring Boot applications with libraries including
JUnit, Hamcrest and Mockitodescription>
<url>https://projects.spring.io/spring-boot/#/spring-boot-parent/spring-boot-starters/spring-boot-starter-testurl>
<organization>
<name>Pivotal Software, Inc.name>
<url>https://spring.iourl>
organization>
<licenses>
<license>
<name>Apache License, Version 2.0name>
<url>http://www.apache.org/licenses/LICENSE-2.0url>
license>
licenses>
<developers>
<developer>
<name>Pivotalname>
<email>[email protected]email>
<organization>Pivotal Software, Inc.organization>
<organizationUrl>http://www.spring.ioorganizationUrl>
developer>
developers>
<scm>
<connection>scm:git:git://github.com/spring-projects/spring-boot.git/spring-boot-starters/spring-boot-starter-testconnection>
<developerConnection>scm:git:ssh://[email protected]/spring-projects/spring-boot.git/spring-boot-starters/spring-boot-starter-testdeveloperConnection>
<url>http://github.com/spring-projects/spring-boot/spring-boot-starters/spring-boot-starter-testurl>
scm>
<issueManagement>
<system>Githubsystem>
<url>https://github.com/spring-projects/spring-boot/issuesurl>
issueManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
<version>2.1.0.RELEASEversion>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-testartifactId>
<version>2.1.0.RELEASEversion>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-test-autoconfigureartifactId>
<version>2.1.0.RELEASEversion>
<scope>compilescope>
dependency>
<dependency>
<groupId>com.jayway.jsonpathgroupId>
<artifactId>json-pathartifactId>
<version>2.4.0version>
<scope>compilescope>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.assertjgroupId>
<artifactId>assertj-coreartifactId>
<version>3.11.1version>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.mockitogroupId>
<artifactId>mockito-coreartifactId>
<version>2.23.0version>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.hamcrestgroupId>
<artifactId>hamcrest-coreartifactId>
<version>1.3version>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.hamcrestgroupId>
<artifactId>hamcrest-libraryartifactId>
<version>1.3version>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.skyscreamergroupId>
<artifactId>jsonassertartifactId>
<version>1.5.0version>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-coreartifactId>
<version>5.1.2.RELEASEversion>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-testartifactId>
<version>5.1.2.RELEASEversion>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.xmlunitgroupId>
<artifactId>xmlunit-coreartifactId>
<version>2.6.2version>
<scope>compilescope>
dependency>
dependencies>
project>
下面是出现频率极高的注解:
/*
* 这个注解的作用是,在执行单元测试的时候,不是直接去执行里面的单元测试的方法
* 因为那些方法执行之前,是需要做一些准备工作的,它是需要先初始化一个spring容器的
* 所以得找这个SpringRunner这个类,来先准备好spring容器,再执行各个测试方法
*/
@RunWith(SpringRunner.class)
/*
* 这个注解的作用是,去寻找一个标注了@SpringBootApplication注解的一个类,也就是启动类
* 然后会执行这个启动类的main方法,就可以创建spring容器,给后面的单元测试提供完整的这个环境
*/
@SpringBootTest
/*
* 这个注解的作用是,可以让每个方法都是放在一个事务里面
* 让单元测试方法执行的这些增删改的操作,都是一次性的
*/
@Transactional
/*
* 这个注解的作用是,如果产生异常那么会回滚,保证数据库数据的纯净
* 默认就是true
*/
@Rollback(true)
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方法 |
@Test
public void haveReturn() {
// 1、初始化数据
// 2、模拟行为
// 3、调用方法
// 4、断言
}
@Test
public void noReturn() {
// 1、初始化数据
// 2、模拟行为
// 3、调用方法
// 4、验证执行次数
}
以常见的SpringMVC3层架构为例,咱们分别展示3层架构如何做简单的单元测试。业务场景为用户user的增删改查。
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;
}
}
@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;
}
}
@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.jacocogroupId>
<artifactId>jacoco-maven-pluginartifactId>
<version>0.8.2version>
dependency>
(2)添加插件
<plugin>
<groupId>org.jacocogroupId>
<artifactId>jacoco-maven-pluginartifactId>
<version>0.8.2version>
<executions>
<execution>
<id>pre-testid>
<goals>
<goal>prepare-agentgoal>
goals>
execution>
<execution>
<id>post-testid>
<phase>testphase>
<goals>
<goal>reportgoal>
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、文章小例源码地址:https://gitee.com/diqirenge/sheep-web-demo/tree/master/sheep-web-demo-junit
3、mockito官网:https://site.mockito.org/
4、mockito中文文档:https://github.com/hehonghui/mockito-doc-zh