您现在的位置是:首页 >其他 >Java单元测试实战(二)编写流程网站首页其他
Java单元测试实战(二)编写流程
版权声明:本文为博主「SJMP1974」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
编辑:SJMP1974
原文出处链接:https://editor.csdn.net/md/?not_checkout=1
参考:https://developer.aliyun.com/ebook/7895?spm=a2c6h.13066369.question.5.e953296fRPnbNA
测试框架简介
Mockito 是一个单元测试模拟框架,可以让你写出优雅、简洁的单元测试代码。
Mockito 采用了模拟技术,模拟了一些在应用中依赖的复杂对象,从而把测试对象
和依赖对象隔离开来。
编写测试用例
引入依赖
为了引入Mockito 和PowerMock 包,需要在maven 项目的pom.xml 文件中加入
以下包依赖:
其中,powermock.version 为2.0.9,为当前的最新版本,可根据实际情况修改。在
PowerMock包中,已经包含了对应的Mockito和JUnit包,所以无需单独引入Mockito
和JUnit 包。
单元测试案例***
- 一个有依赖的单元测试
- 定义对象:定义测试对象,模拟依赖对象、注入依赖对象;
- 模拟方法:模拟参数或返回值、模拟依赖方法;
- 调用方法:传入参数对象、调用测试方法、验证返回值或异常;
- 验证方法:验证依赖方法、验证方法参数、验证依赖对象。
业务代码(可以先大致看下):
/**
* 用户服务类
*/
@Service
public class UserService {
/** 定义依赖对象 */
/** 用户DAO */
@Autowired
private UserDAO userDAO;
/** 标识生成器 */
@Autowired
private IdGenerator idGenerator;
/** 定义依赖参数 */
/** 可以修改 */
@Value("${userService.canModify}")
private Boolean canModify;
/**
* 保存用户
*
* @param userSave 用户保存
* @return 用户标识
*/
public Long saveUser(UserVO userSave) {
// 获取用户标识
Long userId = userDAO.getIdByName(userSave.getName());
// 根据存在处理
// 根据存在处理: 不存在则创建
if (Objects.isNull(userId)) {
userId = idGenerator.next();
UserDO userCreate = new UserDO();
userCreate.setId(userId);
userCreate.setName(userSave.getName());
userCreate.setDescription(userSave.getDescription());
userDAO.create(userCreate);
}
// 根据存在处理: 已存在可修改
else if (Boolean.TRUE.equals(canModify)) {
UserDO userModify = new UserDO();
userModify.setId(userId);
userModify.setName(userSave.getName());
userModify.setDescription(userSave.getDescription());
userDAO.modify(userModify);
}
// 根据存在处理: 已存在禁修改
else {
throw new UnsupportedOperationException("不支持修改");
}
// 返回用户标识
return userId;
}
}
对应的单元测试用例:
代码中的第一个单元测试方法重点看!!!
/**
* 用户服务测试类
*
*/
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
/** 1.1 定义测试对象 */
/** 1.3 InjectMocks 和 Mock 配合可以将 userDAO 注入 userService
用户服务 */
@InjectMocks
private UserService userService;
/** 1.2 模拟依赖对象 */
/** 用户DAO */
@Mock
private UserDAO userDAO;
/** 定义静态常量 */
/** 资源路径 */
private static final String RESOURCE_PATH ="testUserService/";
/** 标识生成器 */
@Mock
private IdGenerator idGenerator;
/**
* 在测试之前
*/
@Before
public void beforeTest() {
Whitebox.setInternalState(userService, "canModify",
Boolean.TRUE);
}
/**
* 测试: 保存用户-创建
*/
@Test
public void testSaveUserWithCreate() {
// 2.1 模拟依赖方法
// 模拟依赖方法: userDAO.getIdByName
Mockito.doReturn(null).when(userDAO).getIdByName(Mockito.anyStri
ng());
// 2.1 模拟依赖方法: idGenerator.next
Long userId = 123L;
Mockito.doReturn(userId).when(idGenerator).next();
// 3.1 调用测试方法
String path = RESOURCE_PATH + "testSaveUserWithCreate/";
String text =
ResourceHelper.getResourceAsString(getClass(), path +
"userSave.json");
UserSaveVO userSave = JSON.parseObject(text,
UserSaveVO.class);
// 3.2 验证返回值或异常
Assert.assertEquals("用户标识不一致", userId,
userService.saveUser(userSave));
// 4.1 验证依赖方法
// 4.1 验证依赖方法: userDAO.getIdByName
Mockito.verify(userDAO).getIdByName(userSave.getName());
// 4.1 验证依赖方法: idGenerator.next
Mockito.verify(idGenerator).next();
// 4.2 验证方法参数: userDAO.create
ArgumentCaptor<UserDO> userCreateCaptor =
ArgumentCaptor.forClass(UserDO.class);
Mockito.verify(userDAO).create(userCreateCaptor.capture());
text = ResourceHelper.getResourceAsString(getClass(),
path + "userCreate.json");
Assert.assertEquals("用户创建不一致", text,
JSON.toJSONString(userCreateCaptor.getValue()));
// 4.3 验证依赖对象,确保所有验证均已覆盖
Mockito.verifyNoMoreInteractions(userDAO, idGenerator);
}
/**
* 测试: 保存用户-修改
*/
@Test
public void testSaveUserWithModify() {
// 模拟依赖方法
// 模拟依赖方法: userDAO.getIdByName
Long userId = 123L;
Mockito.doReturn(userId).when(userDAO).getIdByName(Mockito.anySt
ring());
// 调用测试方法
String path = RESOURCE_PATH + "testSaveUserWithModify/";
String text =
ResourceHelper.getResourceAsString(getClass(), path +
"userSave.json");
UserSaveVO userSave = JSON.parseObject(text,
UserSaveVO.class);
Assert.assertEquals("用户标识不一致", userId,
userService.saveUser(userSave));
Assert.assertEquals("异常消息不一致", "不支持修改",
exception.getMessage());
// 验证依赖方法
// 验证依赖方法: userDAO.getIdByName
Mockito.verify(userDAO).getIdByName(userSave.getName());
// 验证依赖对象
Mockito.verifyNoMoreInteractions(userDAO, idGenerator);
}
}
其中,加载的JSON 资源文件内容如下:
userSave.json:
userCreate.json:
userModify.json:
通过执行以上测试用例,可以看到对源代码进行了100%的行覆盖。
小结:
编写测试用例流程如下:
如上图所示,第1、3 步适用于大多数单元测试,而第2、4 步只适用于有外部依赖的单元测试。
下面将对各个细节进行拆解:
流程详细介绍
定义被测对象
- 直接构建对象,简单直接
- 利用Mockito.spy 方法
Mockito 提供一个spy 功能,用于拦截那些尚未实现或不期望被真实调用的方法,
默认所有方法都是真实方法,除非主动去模拟对应方法。所以,利用spy 功能来定
义被测对象,适合于需要模拟被测类自身方法的情况,适用于普通类、接口和虚基
类。
- 利用 @Spy 注解
@Spy注解跟Mockito.spy 方法一样,可以用来定义被测对象,适合于需要模拟被测类自身方法的情况,适用于普通类、接口和虚基类。@Spy注解需要配合@RunWith注解使用。
- 利用@InjectMocks 注解
@InjectMocks 注解用来创建一个实例,并将其它对象(@Mock、@Spy或直接定义的对象)注入到该实例中。所以,@InjectMocks 注解本身就可以用来定义被测对象。@InjectMocks 注解需要配合@RunWith 注解使用。
模拟依赖对象
在编写单元测试用例时,需要模拟各种依赖对象——类成员、方法参数和方法返回值。
- 直接构建对象
如果需要构建一个对象,最简单直接的方法就是——定义对象并赋值。
- 反序列化对象
如果对象字段或层级非常庞大,采用直接构建对象方法,可能会编写大量构建程序代码。这种情况,可以考虑反序列化对象,将会大大减少程序代码。由于JSON 字符串可读性高,这里就以JSON 为例,介绍反序列化对象。
反序列化模型对象:
反序列化模型对象:
反序列化映射对象:
- 利用 Mockito.mock 方法 Mockito
Mockito 默认所有方法都已被模拟——方法为空并返回默认值(null 或0),除非主动执行doCallRealMethod 或thenCallRealMethod 操作,才能够调用真实的方法。
- 利用@Mock 注解
@Mock 注解跟 Mockito.mock 方法一样,可以用来模拟依赖对象,适用于普通类、接口和虚基类。@Mock 注解需要配合 @RunWith 注解使用。
- 利用Mockito.spy 方法
- 利用@Spy 注解
@Spy注解跟Mockito.spy 方法一样,可以用来模拟依赖对象,适用于普通类、接口和虚基类。@Spy 注解需要配合@RunWith 注解使用。
注入依赖对象
- 利用Setter 方法注入
- 利用ReflectionTestUtils.setField 方法注入
- 利用Whitebox.setInternalState 方法注入
- 利用@InjectMocks 注解注入
@InjectMocks 注解用来创建一个实例,并将其它对象(@Mock、@Spy或直接定义的对象)注入到该实例中。@InjectMocks 注解需要配合@RunWith 注解使用。
- 设置静态常量字段值
Whitebox.setInternalState 方法和@InjectMocks 注解并不支持设置静态常量,需要自己实现一个设置静态常量的方法:
具体使用方法如下:
注意:经过测试,该方法对于int、Integer 等基础类型并不生效,应该是编译器常量优化导致。
模拟依赖对象
- 根据返回模拟方法
- 模拟方法单个返回值
- 模拟方法定制返回值
- 模拟方法抛出单个异常
- 直接调用真实方法
其他省略…
编写时,未知的细节可以查阅手册
调用被测方法
- 调用无权限访问的普通方法
调用无访问权限的普通方法,可以使用PowerMock提供的Whitebox.invokeMethod方法。
- 调用无权限访问的静态方法
…
验证依赖方法
在单元测试中,验证是确认模拟的依赖方法是否按照预期被调用或未调用的过程。
- 验证无参数方法调用
验证方法默认调用1次
- 验证指定参数方法调用
- 验证任意参数方法调用
在验证依赖方法时,有时候并不关心传入参数的具体值,可以使用Mockito 参数匹配器的any 方法。Mockito 提供了anyInt、anyLong、anyString、anyList、anySet、anyMap、any(Class clazz)等方法来表示任意值。
- 验证必空参数方法调用
同样,如果要匹配null 对象,可以使用isNull 方法,或使用eq(null)。
- 验证方法调用n次
- 验证方法调用至少1次
- 使用ArgumentCaptor.forClass方法定义参数捕获器
使用@Captor 注解定义参数捕获器
验证数据对象
- 通过JUnit 提供的Assert.assertNull 方法验证数据对象为空。
- 验证数据对象值
JUnit 提供Assert.assertEquals、Assert.assertNotEquals、Assert.assertArrayEquals方法组,可以用来验证数据对象值是否相等。
- 验证复杂数组或集合对象
对于复杂的JavaBean 数组和集合对象,需要先展开数组和集合对象中每一个JavaBean 数据对象,然后验证JavaBean 数据对象的每一个属性字段。
- 通过序列化验证数据对象
如上一节例子所示,当数据对象过于复杂时,如果采用Assert.assertEquals 依次验证每个JavaBean 对象、验证每一个属性字段,测试用例的代码量将会非常庞大。这里,推荐使用序列化手段简化数据对象的验证,比如利用JSON.toJSONString 方法把复杂的数据对象转化为字符串,然后再使用Assert.assertEquals 方法进行验证字符串。但是,序列化值必须具备有序性、一致性和可读性。
通常使用JSON.toJSONString 方法把Map 对象转化为字符串,其中key-value 的顺序具有不确定性,无法用于验证两个对象是否一致。这里,JSON 提供序列化选项SerializerFeature.MapSortField(映射排序字段),可以用于保证序列化后的keyvalue的有序性。
- 验证数据对象私有属性字段
有时候,单元测试用例需要对复杂对象的私有属性字段进行验证。而PowerMockito提供的Whitebox.getInternalState 方法,获取轻松地获取到私有属性字段值。
验证依赖对象
- 验证模拟对象没有任何方法调用
Mockito 提供了verifyNoInteractions 方法,可以验证模拟对象在被测试方法中没有任何调用。
- 验证模拟对象没有更多方法调用
Mockito 提供了verifyNoMoreInteractions 方法,在验证模拟对象所有方法调用后使用,可以验证模拟对象所有方法调用是否都得到验证。如果模拟对象存在任何未验证的方法调用,就会抛出NoInteractionsWanted 异常。
- 清除模拟对象所有方法调用标记
在编写单元测试用例时,为了减少单元测试用例数和代码量,可以把多组参数定义在同一个单元测试用例中,然后用for 循环依次执行每一组参数的被测方法调用。为了避免上一次测试的方法调用影响下一次测试的方法调用验证,最好使用Mockito 提供clearInvocations 方法清除上一次的方法调用。