中发现大家都知道单元测试,也知道 junit,但是没有人知道怎么写 junit 单元测试,在这里分享我在工作中是怎么写单元测试的,供大家参考
什么是单元测试
首先讲讲什么是单元测试,单元测试是指对软件中的最小可测试单元进行检查和验证。单元测试在质量保证中是非常重要的环节,根据测试金字塔模型,越往上层的测试,所需的测试投入比例越大,效果也越差,而单元测试的成本要小的多,也更容易发现问题
单元测试的过程
完整的单元测试包括上面几个过程
数据准备
某些方法需要数据库初始化一些数据才能正常执行(如获取公众号配置信息,公众号配置信息在项目初始化的时候插如到数据库中的),在执行单元测试时,经常遇到由于所依赖的数据不存在或被修改了,或者在新的环境下,所依赖的数据库不存在,进而导致单元测试不通过
针对数据不存在/修改的情况,需要在执行测试用例前,初始化需要依赖到的一些数据,一般是数据库数据。而对于数据库不存在的情况,则使用H2
内存数据库来模拟mysql
环境来解决
导入H2
依赖
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2.version}</version>
<scope>test</scope>
</dependency>
/src/test/resources/application.yml 配置H2数据库
spring:
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:mmall;MODE=MySQL
# 表结构初始化脚本,多个用逗号分割
schema: classpath:db/user_schemas.sql
/src/test/resources/user_schemas.sql
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(50) NOT NULL COMMENT '用户密码',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
单元测试代码
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
// 使用Transaction清理数据
@Transaction
public void selectByIdTest(){
// 数据准备
User user = new User();
user.setId(1);
user.setUsername("张三");
user.setPassword("123");
userService.save(user);
// 执行测试+结果验证
assertNotNull(userService.selectById(1));
}
}
有时候
H2
并不能完全模拟mysql
,因为某些mysql
特性/函数在H2
中并没有,就得专门搭一个测试用的mysql
来跑单元测试,为避免这种情况导致误诊,有条件的情况下建议搭一个测试用mysql
参数构造
构造调用被测方法需要传入的参数
执行测试
这一步比较简单,即执行被测方法
验证结果
验证结果返回值的正确性,统一使用junit4.4
提供的assertThat
断言语法,不再使用之前的assertion
语句(如assertEquals
、assertNotSame
、assertTrue
、assertNotNull
等),assertThat
语法如下
assertThat( [value], [matcher statement] );
相比于assertion
,使用assertThat
语法有以下优点
- 代码风格统一
assertThat
可以替代所有的assertion
语句,这样可以在所有的单元测试中只使用一个断言方法,使得编写测试用例变得简单,代码风格变得统一,测试代码也更容易维护 - 支持强大的
Matcher
匹配符Matcher
匹配符具有很强的易读性,使用起来更加灵活,如:想判断某个字符串 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")));
- 更加易懂的日志信息
相比assertion
语句,asserThat
提供的错误信息更加易懂,便于排查问题,同样是判空断言不通过,assertion
assertNotNull(userMapper.selectById(3));
从报错信息只能看出是
AssertionError
,无法知道执行结果与期望值
assertThat(userMapper.selectById(3),notNullValue());
不仅说明了错误类型是
AssertionError
,而且还打印出了期望值和执行结果,更容易排查问题
数据清理
清理第一步准备的数据,有可能影响到下一步的测试
对于支持回滚的数据库(如mysql),可使用@Transactional
注解回滚数据,对于不支持回滚操作的(如redis)则需要在测试方法的最后手动清理
使用Mock框架进行测试
很多情况下,尤其在微服务架构下,被测方法往往会调用一些第三方服务,这时候当依赖的第三方服务不稳定,就会导致单元测试执行失败。这时候就需要对依赖的第三方服务进行mock
,使其返回正确的结果
class UserServiceImpl implements UserService {
private final AudienceClient audienceClient;
public UserServiceImpl(AudienceClient audienceClient){
this.audienceClient = audienceClient;
}
public User get(Long id) {
AudienceModel audienceModel = AudienceClient.getById(1);
// 省略...
return user;
}
}
@SpringBootTest
@RunWith(SpringRunner.class)
class UserServiceImplTest {
@InjectMocks // 注入 UserServiceImpl bean
private UserServiceImpl userService;
@Mock // mock掉AudienceClient
private AudienceClient audienceClient;
@Test
void get() {
AudienceModel model = new AudienceModel();
model.setId(1);
model.setNickName("hxy");
// mock 掉audienceClient 方法,使其放回预期结果
Mockito.when(audienceClient.getById(1)).thenReturn(model);
assertThat(userService.get(1),notNullValue());
}
}
项目代码一般分为 controller、service、dao 三层。在测试 controller 的时候,由于 controller 会调用 service,service 又会调用 dao 层。当我们测试 controller 的时候,往往由于 service 层的报错导致 controller 测试不通过
controller 层主要测试参数校验逻辑,测试的时候并不需要真正的调用 service 层服务,可以通过 mock 框架将 service 方法 mock 掉
@SpringBootTest
@RunWith(SpringRunner.class)
public class UserControllerTest {
private MockMvc mvc;
@InjectMocks
private UserController userController;
@Mock
private UserService userService;
@Before
public void init() {
mvc = MockMvcBuilders.standaloneSetup(userController)
.build();
}
@Test
public void get() throws Exception {
Mockito.when(userService.get(1)).thenReturn(new User());
mvc.perform(MockMvcRequestBuilders.get("/user/get?id=1")
.characterEncoding("utf-8")
.accept(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk())
// .andExpect() 根据需要可添加多个andExpect
.andDo(MockMvcResultHandlers.print());
}
}
service 层主要测试业务逻辑是否正确,当 service 层发生调用外部服务的时候,需要 mock 掉外部服务的调用代码,避免单元测试的时候调用外部服务,导致外部服务异常。同时单元测试不应该依赖外部服务
@SpringBootTest
@RunWith(SpringRunner.class)
class UserServiceImplTest {
@InjectMocks
private UserServiceImpl userService;
@Mock
private AudienceClient audienceClient;
@Test
void get() {
Mockito.when(audienceClient.selectById(1)).thenReturn(new User());
assertThat(userService.get(1),notNullValue());
}
}
service 层调用 dao 层,无需 mock 掉 dao层,因为使用 mybatis-plus 有很多代码是写在 service 层的,dao 层无法单独抽出来测试,所以可以将 service 层跟 dao 层合并进行测试