编写单元测试案例流程的 Java 编程技能
作者 | 常意
来源 | 阿里技术公众号
温馨提示:本文较长,同学们可收藏后再看 :)
前言
清代杰出思想家章学诚有一句名言:“学必求其心得,业必贵其专精。”
意思是:学习上一定要追求心得体会,事业上一定要贵以专注精深。做技术就是这样,一件事如果做到了极致,就必然会有所心得体会。作者最近在一个项目上,追求单元测试覆盖率到极致,所以才有了这篇心得体会。
上一篇文章《Java单元测试技巧之PowerMock》除了介绍单元测试基础知识外,主要介绍了“为什么要编写单元测试”。很多同学读完后,还是不能快速地编写单元测试用例。而这篇文章,立足于“如何来编写单元测试用例”,能够让同学们“有章可循”,能快速地编写出单元测试用例。
1.编写单元测试用例
1.1.测试框架简介
Mockito是一个单元测试模拟框架,可以让你写出优雅、简洁的单元测试代码。Mockito采用了模拟技术,模拟了一些在应用中依赖的复杂对象,从而把测试对象和依赖对象隔离开来。
PowerMock是一个单元测试模拟框架,是在其它单元测试模拟框架的基础上做出扩展。 通过提供定制的类加载器以及一些字节码篡改技术的应用,PowerMock实现了对静态方法、构造方法、私有方法以及final方法的模拟支持等强大的功能。但是,正因为PowerMock进行了字节码篡改,导致部分单元测试用例并不被JaCoco统计覆盖率。
通过作者多年单元测试的编写经验,优先推荐使用Mockito提供的功能;只有在Mockito提供的功能不能满足需求时,才会采用PowerMock提供的功能;但是,不推荐使用影响JaCoco统计覆盖率的PowerMock功能。在本文中,我们也不会对影响JaCoco统计覆盖率的PowerMock功能进行介绍。
下面,将以Mockito为主、以PowerMock为辅,介绍一下如何编写单元测试用例。
1.2.测试框架引入
为了引入Mockito和PowerMock包,需要在maven项目的pom.xml文件中加入以下包依赖:
其中,powermock.version为2.0.9,为当前的最新版本,可根据实际情况修改。在PowerMock包中,已经包含了对应的Mockito和JUnit包,所以无需单独引入Mockito和JUnit包。
1.3.典型代码案例
一个典型的服务代码案例如下:
/**
* 用户服务类
*/
@Service
public class UserService {
/** 服务相关 */
/** 用户DAO */
@Autowired
private UserDAO userDAO;
/** 标识生成器 */
@Autowired
private IdGenerator idGenerator;
/** 参数相关 */
/** 可以修改 */
@Value("${userService.canModify}")
private Boolean canModify;
/**
* 创建用户
*
* @param userCreate 用户创建
* @return 用户标识
*/
public Long createUser(UserVO userCreate) {
// 获取用户标识
Long userId = userDAO.getIdByName(userCreate.getName());
// 根据存在处理
// 根据存在处理: 不存在则创建
if (Objects.isNull(userId)) {
userId = idGenerator.next();
UserDO create = new UserDO();
create.setId(userId);
create.setName(userCreate.getName());
userDAO.create(create);
}
// 根据存在处理: 已存在可修改
else if (Boolean.TRUE.equals(canModify)) {
UserDO modify = new UserDO();
modify.setId(userId);
modify.setName(userCreate.getName());
userDAO.modify(modify);
}
// 根据存在处理: 已存在禁修改
else {
throw new UnsupportedOperationException("不支持修改");
}
// 返回用户标识
return userId;
}
}
1.4.测试用例编写
采用Mockito和PowerMock单元测试模拟框架,编写的单元测试用例如下:
UserServiceTest.java:
/**
* 用户服务测试类
*/
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
/** 模拟依赖对象 */
/** 用户DAO */
@Mock
private UserDAO userDAO;
/** 标识生成器 */
@Mock
private IdGenerator idGenerator;
/** 定义被测对象 */
/** 用户服务 */
@InjectMocks
private UserService userService;
/**
* 在测试之前
*/
@Before
public void beforeTest() {
// 注入依赖对象
Whitebox.setInternalState(userService, "canModify", Boolean.TRUE);
}
/**
* 测试: 创建用户-新
*/
@Test
public void testCreateUserWithNew() {
// 模拟依赖方法
// 模拟依赖方法: userDAO.getByName
Mockito.doReturn(null).when(userDAO).getIdByName(Mockito.anyString());
// 模拟依赖方法: idGenerator.next
Long userId = 1L;
Mockito.doReturn(userId).when(idGenerator).next();
// 调用被测方法
String text = ResourceHelper.getResourceAsString(getClass(), "userCreateVO.json");
UserVO userCreate = JSON.parseObject(text, UserVO.class);
Assert.assertEquals("用户标识不一致", userId, userService.createUser(userCreate));
// 验证依赖方法
// 验证依赖方法: userDAO.getByName
Mockito.verify(userDAO).getIdByName(userCreate.getName());
// 验证依赖方法: idGenerator.next
Mockito.verify(idGenerator).next();
// 验证依赖方法: userDAO.create
ArgumentCaptor < UserDO> userCreateCaptor = ArgumentCaptor.forClass(UserDO.class);
Mockito.verify(userDAO).create(userCreateCaptor.capture());
text = ResourceHelper.getResourceAsString(getClass(), "userCreateDO.json");
Assert.assertEquals("用户创建不一致", text, JSON.toJSONString(userCreateCaptor.getValue()));
// 验证依赖对象
Mockito.verifyNoMoreInteractions(idGenerator, userDAO);
}
/**
* 测试: 创建用户-旧
*/
@Test
public void testCreateUserWithOld() {
// 模拟依赖方法
// 模拟依赖方法: userDAO.getByName
Long userId = 1L;
Mockito.doReturn(userId).when(userDAO).getIdByName(Mockito.anyString());
// 调用被测方法
String text = ResourceHelper.getResourceAsString(getClass(), "userCreateVO.json");
UserVO userCreate = JSON.parseObject(text, UserVO.class);
Assert.assertEquals("用户标识不一致", userId, userService.createUser(userCreate));
// 验证依赖方法
// 验证依赖方法: userDAO.getByName
Mockito.verify(userDAO).getIdByName(userCreate.getName());
// 验证依赖方法: userDAO.modify
ArgumentCaptor < UserDO> userModifyCaptor = ArgumentCaptor.forClass(UserDO.class);
Mockito.verify(userDAO).modify(userModifyCaptor.capture());
text = ResourceHelper.getResourceAsString(getClass(), "userModifyDO.json");
Assert.assertEquals("用户修改不一致", text, JSON.toJSONString(userModifyCaptor.getValue()));
// 验证依赖对象
Mockito.verifyNoInteractions(idGenerator);
Mockito.verifyNoMoreInteractions(userDAO);
}
/**
* 测试: 创建用户-异常
*/
@Test
public void testCreateUserWithException() {
// 注入依赖对象
Whitebox.setInternalState(userService, "canModify", Boolean.FALSE);
// 模拟依赖方法
// 模拟依赖方法: userDAO.getByName
Long userId = 1L;
Mockito.doReturn(userId).when(userDAO).getIdByName(Mockito.anyString());
// 调用被测方法
String text = ResourceHelper.getResourceAsString(getClass(), "userCreateVO.json");
UserVO userCreate = JSON.parseObject(text, UserVO.class);
UnsupportedOperationException exception = Assert.assertThrows("返回异常不一致",
UnsupportedOperationException.class, () -> userService.createUser(userCreate));
Assert.assertEquals("异常消息不一致", "不支持修改", exception.getMessage());
}
}
userCreateVO.json:
{"name":"test"}
userCreateDO.json:
{"id":1,"name":"test"}
userModifyDO.json:
{"id":1,"name":"test"}
通过执行以上测试用例,可以看到对源代码进行了100%的行覆盖。
2.测试用例编写流程
通过上一章编写Java类单元测试用例的实践,可以总结出以下Java类单元测试用例的编写流程:
单元测试用例编写流程
上面一共有3个测试用例,这里仅以测试用例testCreateUserWithNew(测试: 创建用户-新)为例说明。
2.1.定义对象阶段
第1步是定义对象阶段,主要包括定义被测对象、模拟依赖对象(类成员)、注入依赖对象(类成员)3大部分。
2.1.1.定义被测对象
在编写单元测试时,首先需要定义被测对象,或直接初始化、或通过Spy包装……其实,就是把被测试服务类进行实例化。
/** 定义被测对象 */
/** 用户服务 */
@InjectMocks
private UserService userService;
2.1.2.模拟依赖对象(类成员)
在一个服务类中,我们定义了一些类成员对象——服务(Service)、数据访问对象(DAO)、参数(Value)等。在Spring框架中,这些类成员对象通过@Autowired、@Value等方式注入,它们可能涉及复杂的环境配置、依赖第三方接口服务……但是,在单元测试中,为了解除对这些类成员对象的依赖,我们需要对这些类成员对象进行模拟。
/** 模拟依赖对象 */
/** 用户DAO */
@Mock
private UserDAO userDAO;
/** 标识生成器 */
@Mock
private IdGenerator idGenerator;
2.1.3.注入依赖对象(类成员)
当模拟完这些类成员对象后,我们需要把这些类成员对象注入到被测试类的实例中。以便在调用被测试方法时,可能使用这些类成员对象,而不至于抛出空指针异常。
/** 定义被测对象 */
/** 用户服务 */
@InjectMocks
private UserService userService;
/**
* 在测试之前
*/
@Before
public void beforeTest() {
// 注入依赖对象
Whitebox.setInternalState(userService, "canModify", Boolean.TRUE);
}
2.2.模拟方法阶段
第2步是模拟方法阶段,主要包括模拟依赖对象(参数或返回值)、模拟依赖方法2大部分。
2.2.1.模拟依赖对象(参数或返回值)
通常,在调用一个方法时,需要先指定方法的参数,然后获取到方法的返回值。所以,在模拟方法之前,需要先模拟该方法的参数和返回值。
Long userId = 1L;
2.2.2.模拟依赖方法
在模拟完依赖的参数和返回值后,就可以利用Mockito和PowerMock的功能,进行依赖方法的模拟。如果依赖对象还有方法调用,还需要模拟这些依赖对象的方法。
// 模拟依赖方法
// 模拟依赖方法: userDAO.getByName
Mockito.doReturn(null).when(userDAO).getIdByName(Mockito.anyString());
// 模拟依赖方法: idGenerator.next
Mockito.doReturn(userId).when(idGenerator).next();
2.3.调用方法阶段
第3步是调用方法阶段,主要包括模拟依赖对象(参数)、调用被测方法、验证参数对象(返回值)3步。
2.3.1.模拟依赖对象(参数)
在调用被测方法之前,需要模拟被测方法的参数。如果这些参数还有方法调用,还需要模拟这些参数的方法。
String text = ResourceHelper.getResourceAsString(getClass(), "userCreateVO.json");
UserVO userCreate = JSON.parseObject(text, UserVO.class);
2.3.2.调用被测方法
在准备好参数对象后,就可以调用被测试方法了。如果被测试方法有返回值,需要定义变量接收返回值;如果被测试方法要抛出异常,需要指定期望的异常。
userService.createUser(userCreate)
2.3.3.验证数据对象(返回值)
在调用被测试方法后,如果被测试方法有返回值,需要验证这个返回值是否符合预期;如果被测试方法要抛出异常,需要验证这个异常是否满足要求。
Assert.assertEquals("用户标识不一致", userId, userService.createUser(userCreate));
2.4.验证方法阶段
第4步是验证方法阶段,主要包括验证依赖方法、验证数据对象(参数)、验证依赖对象3步。
2.4.1.验证依赖方法
作为一个完整的测试用例,需要对每一个模拟的依赖方法调用进行验证。
// 验证依赖方法
// 验证依赖方法: userDAO.getByName
Mockito.verify(userDAO).getIdByName(userCreate.getName());
// 验证依赖方法: idGenerator.next
Mockito.verify(idGenerator).next();
// 验证依赖方法: userDAO.create
ArgumentCaptor < UserDO> userCreateCaptor = ArgumentCaptor.forClass(UserDO.class);
Mockito.verify(userDAO).create(userCreateCaptor.capture());
2.4.2.验证数据对象(参数)
对应一些模拟的依赖方法,有些参数对象是被测试方法内部生成的。为了验证代码逻辑的正确性,就需要对这些参数对象进行验证,看这些参数对象值是否符合预期。
text = ResourceHelper.getResourceAsString(getClass(), "userCreateDO.json");
Assert.assertEquals("用户创建不一致", text, JSON.toJSONString(userCreateCaptor.getValue()));
2.4.3.验证依赖对象
作为一个完整的测试用例,应该保证每一个模拟的依赖方法调用都进行了验证。正好,Mockito提供了一套方法,用于验证模拟对象所有方法调用都得到了验证。
// 验证依赖对象
Mockito.verifyNoMoreInteractions(idGenerator, userDAO);
3.定义被测对象
在编写单元测试时,首先需要定义被测对象,或直接初始化、或通过Spy包装……其实,就是把被测试服务类进行实例化。
3.1.直接构建对象
直接构建一个对象,总是简单又直接。
UserService userService = new UserService();
3.2.利用Mockito.spy方法
Mockito提供一个spy功能,用于拦截那些尚未实现或不期望被真实调用的方法,默认所有方法都是真实方法,除非主动去模拟对应方法。所以,利用spy功能来定义被测对象,适合于需要模拟被测类自身方法的情况,适用于普通类、接口和虚基类。
UserService userService = Mockito.spy(new UserService());
UserService userService = Mockito.spy(UserService.class);
AbstractOssService ossService = Mockito.spy(AbstractOssService.class);
3.3.利用@Spy注解
@Spy注解跟Mockito.spy方法一样,可以用来定义被测对象,适合于需要模拟被测类自身方法的情况,适用于普通类、接口和虚基类。@Spy注解需要配合@RunWith注解使用。
@RunWith(PowerMockRunner.class)
public class CompanyServiceTest {
@Spy
private UserService userService = new UserService();
...
}
注意:@Spy注解对象需要初始化。如果是虚基类或接口,可以用Mockito.mock方法实例化。
3.4.利用@InjectMocks注解
@InjectMocks注解用来创建一个实例,并将其它对象(@Mock、@Spy或直接定义的对象)注入到该实例中。所以,@InjectMocks注解本身就可以用来定义被测对象。@InjectMocks注解需要配合@RunWith注解使用。
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
@InjectMocks
private UserService userService;
...
}
4.模拟依赖对象
在编写单元测试用例时,需要模拟各种依赖对象——类成员、方法参数和方法返回值。
4.1.直接构建对象
如果需要构建一个对象,最简单直接的方法就是——定义对象并赋值。
Long userId = 1L;
String userName = "admin";
UserDO user = new User();
user.setId(userId);
user.setName(userName);
List < Long> userIdList = Arrays.asList(1L, 2L, 3L);
4.2.反序列化对象
如果对象字段或层级非常庞大,采用直接构建对象方法,可能会编写大量构建程序代码。这种情况,可以考虑反序列化对象,将会大大减少程序代码。由于JSON字符串可读性高,这里就以JSON为例,介绍反序列化对象。
反序列化模型对象:
String text = ResourceHelper.getResourceAsString(getClass(), "user.json");
UserDO user = JSON.parseObject(text, UserDO.class);
反序列化集合对象:
String text = ResourceHelper.getResourceAsString(getClass(), "userList.json");
List < UserDO> userList = JSON.parseArray(text, UserDO.class);
反序列化映射对象:
String text = ResourceHelper.getResourceAsString(getClass(), "userMap.json");
Map < Long, UserDO> userMap = JSON.parseObject(text, new TypeReference < Map < Long, UserDO>>() {});
4.3.利用Mockito.mock方法
Mockito提供一个mock功能,用于拦截那些尚未实现或不期望被真实调用的方法,默认所有方法都已被模拟——方法为空并返回默认值(null或0),除非主动执行doCallRealMethod或thenCallRealMethod操作,才能够调用真实的方法。
利用Mockito.mock方法模拟依赖对象,主要用于以下几种情形:
- 只使用类实例,不使用类属性;
- 类属性太多,但使用其中少量属性(可以mock属性返回值);
- 类是接口或虚基类,并不关心其具体实现类。
MockClass mockClass = Mockito.mock(MockClass.class);
List < Long> userIdList = (List < Long>)Mockito.mock(List.class);
4.4.利用@Mock注解
@Mock注解跟Mockito.mock方法一样,可以用来模拟依赖对象,适用于普通类、接口和虚基类。@Mock注解需要配合@RunWith注解使用。
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
@Mock
private UserDAO userDAO;
...
}
4.5.利用Mockito.spy方法
Mockito.spy方法跟Mockito.mock方法功能相似,只是Mockito.spy方法默认所有方法都是真实方法,除非主动去模拟对应方法。
UserService userService = Mockito.spy(new UserService());
UserService userService = Mockito.spy(UserService.class);
AbstractOssService ossService = Mockito.spy(AbstractOssService.class);
4.6.利用@Spy注解
@Spy注解跟Mockito.spy方法一样,可以用来模拟依赖对象,适用于普通类、接口和虚基类。@Spy注解需要配合@RunWith注解使用。
@RunWith(PowerMockRunner.class)
public class CompanyServiceTest {
@Spy
private UserService userService = new UserService();
...
}
注意:@Spy注解对象需要初始化。如果是虚基类或接口,可以用Mockito.mock方法实例化。
5.注入依赖对象
当模拟完这些类成员对象后,我们需要把这些类成员对象注入到被测试类的实例中。以便在调用被测试方法时,可能使用这些类成员对象,而不至于抛出空指针异常。
5.1.利用Setter方法注入
如果类定义了Setter方法,可以直接调用方法设置字段值。
userService.setMaxCount(100);
userService.setUserDAO(userDAO);
5.2.利用ReflectionTestUtils.setField方法注入
JUnit提供ReflectionTestUtils.setField方法设置属性字段值。
ReflectionTestUtils.setField(userService, "maxCount", 100);
ReflectionTestUtils.setField(userService, "userDAO", userDAO);
5.3.利用Whitebox.setInternalState方法注入
PowerMock提供Whitebox.setInternalState方法设置属性字段值。
Whitebox.setInternalState(userService, "maxCount", 100);
Whitebox.setInternalState(userService, "userDAO", userDAO);
5.4.利用@InjectMocks注解注入
@InjectMocks注解用来创建一个实例,并将其它对象(@Mock、@Spy或直接定义的对象)注入到该实例中。@InjectMocks注解需要配合@RunWith注解使用。
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
@Mock
private UserDAO userDAO;
private Boolean canModify;
@InjectMocks
private UserService userService;
...
}
5.5.设置静态常量字段值
有时候,我们需要对静态常量对象进行模拟,然后去验证是否执行了对应分支下的方法。比如:需要模拟Lombok的@Slf4j生成的log静态常量。但是,Whitebox.setInternalState方法和@InjectMocks注解并不支持设置静态常量,需要自己实现一个设置静态常量的方法:
public final class FieldHelper {
public static void setStaticFinalField(Class< ?> clazz, String fieldName, Object fieldValue) throws NoSuchFieldException, IllegalAccessException {
Field field = clazz.getDeclaredField(fieldName);
FieldUtils.removeFinalModifier(field);
FieldUtils.writeStaticField(field, fieldValue, true);
}
}
具体使用方法如下:
FieldHelper.setStaticFinalField(UserService.class, "log", log);
注意:经过测试,该方法对于int、Integer等基础类型并不生效,应该是编译器常量优化导致。
6.模拟依赖方法
在模拟完依赖的参数和返回值后,就可以利用Mockito和PowerMock的功能,进行依赖方法的模拟。如果依赖对象还有方法调用,还需要模拟这些依赖对象的方法。
6.1.根据返回模拟方法
6.1.1.模拟无返回值方法
Mockito.doNothing().when(userDAO).delete(userId);
6.1.2.模拟方法单个返回值
Mockito.doReturn(user).when(userDAO).get(userId);
Mockito.when(userDAO.get(userId)).thenReturn(user);
6.1.3.模拟方法多个返回值
直接列举出多个返回值:
Mockito.doReturn(record0, record1, record2, null).when(recordReader).read();
Mockito.when(recordReader.read()).thenReturn(record0, record1, record2, null);
转化列表为多个返回值:
List< Record> recordList = ...;
Mockito.doReturn(recordList.get(0), recordList.subList(1, recordList.size()).toArray()).when(recordReader).read();
Mockito.when(recordReader.read()).thenReturn(recordList.get(0), recordList.subList(1, recordList.size()).toArray());
6.1.4.模拟方法定制返回值
可利用Answer定制方法返回值:
Map< Long, UserDO> userMap = ...;
Mockito.doAnswer(invocation -> userMap.get(invocation.getArgument(0)))
.when(userDAO).get(Mockito.anyLong());
Mockito.when(userDAO.get(Mockito.anyLong()))
.thenReturn(invocation -> userMap.get(invocation.getArgument(0)));
Mockito.when(userDAO.get(Mockito.anyLong()))
.then(invocation -> userMap.get(invocation.getArgument(0)));
6.1.5.模拟方法抛出单个异常
指定单个异常类型:
Mockito.doThrow(PersistenceException.class).when(userDAO).get(Mockito.anyLong());
Mockito.when(userDAO.get(Mockito.anyLong())).thenThrow(PersistenceException.class);
指定单个异常对象:
Mockito.doThrow(exception).when(userDAO).get(Mockito.anyLong());
Mockito.when(userDAO.get(Mockito.anyLong())).thenThrow(exception);
6.1.6.模拟方法抛出多个异常
指定多个异常类型:
Mockito.doThrow(PersistenceException.class, RuntimeException.class).when(userDAO).get(Mockito.anyLong());
Mockito.when(userDAO.get(Mockito.anyLong())).thenThrow(PersistenceException.class, RuntimeException.class);
指定多个异常对象:
Mockito.doThrow(exception1, exception2).when(userDAO).get(Mockito.anyLong());
Mockito.when(userDAO.get(Mockito.anyLong())).thenThrow(exception1, exception2);
6.1.7.直接调用真实方法
Mockito.doCallRealMethod().when(userService).getUser(userId);
Mockito.when(userService.getUser(userId)).thenCallRealMethod();
6.2.根据参数模拟方法
Mockito提供do-when语句和when-then语句模拟方法。
6.2.1.模拟无参数方法
对于无参数的方法模拟:
Mockito.doReturn(deleteCount).when(userDAO).deleteAll();
Mockito.when(userDAO.deleteAll()).thenReturn(deleteCount);
6.2.2.模拟指定参数方法
对于指定参数的方法模拟:
Mockito.doReturn(user).when(userDAO).get(userId);
Mockito.when(userDAO.get(userId)).thenReturn(user);
6.2.3.模拟任意参数方法
在编写单元测试用例时,有时候并不关心传入参数的具体值,可以使用Mockito参数匹配器的any方法。Mockito提供了anyInt、anyLong、anyString、anyList、anySet、anyMap、any(Class clazz)等方法来表示任意值。
Mockito.doReturn(user).when(userDAO).get(Mockito.anyLong());
Mockito.when(userDAO.get(Mockito.anyLong())).thenReturn(user);
6.2.4.模拟可空参数方法
Mockito参数匹配器的any具体方法,并不能够匹配null对象。而Mockito提供一个nullable方法,可以匹配包含null对象的任意对象。此外,Mockito.any()方法也可以用来匹配可空参数。
Mockito.doReturn(user).when(userDAO)
.queryCompany(Mockito.anyLong(), Mockito.nullable(Long.class));
Mockito.when(userDAO.queryCompany(Mockito.anyLong(), Mockito< Long>.any()))
.thenReturn(user);
6.2.5.模拟必空参数方法
同样,如果要匹配null对象,可以使用isNull方法,或使用eq(null)。
Mockito.doReturn(user).when(userDAO).queryCompany(Mockito.anyLong(), Mockito.isNull());
Mockito.when(userDAO.queryCompany(Mockito.anyLong(), Mockito.eq(null))).thenReturn(user);
6.2.6.模拟不同参数方法
Mockito支持按不同的参数分别模拟同一方法。
Mockito.doReturn(user1).when(userDAO).get(1L);
Mockito.doReturn(user2).when(userDAO).get(2L);
...
注意:如果一个参数满足多个模拟方法条件,会以最后一个模拟方法为准。
6.2.7.模拟可变参数方法
对于一些变长度参数方法,可以按实际参数个数进行模拟:
Mockito.when(userService.delete(Mockito.anyLong()).thenReturn(true);
Mockito.when(userService.delete(1L, 2L, 3L).thenReturn(true);
也可以用Mockito.any()模拟一个通用匹配方法:
Mockito.when(userService.delete(Mockito.< Long>any()).thenReturn(true);
注意:Mockito.< T>any()并不等于Mockito.any(Class< T> type),前者可以匹配null和类型T的可变参数,后者只能匹配T必填参数。
6.3.模拟其它特殊方法
6.3.1.模拟final方法
PowerMock提供对final方法的模拟,方法跟模拟普通方法一样。但是,需要把对应的模拟类添加到@PrepareForTest注解中。
// 添加@PrepareForTest注解
@PrepareForTest({UserService.class})
// 跟模拟普通方法完全一致
Mockito.doReturn(userId).when(idGenerator).next();
Mockito.when(idGenerator.next()).thenReturn(userId);
6.3.2.模拟私有方法
PowerMock提供提对私有方法的模拟,但是需要把私有方法所在的类放在@PrepareForTest注解中。
PowerMockito.doReturn(true).when(UserService.class, "isSuper", userId);
PowerMockito.when(UserService.class, "isSuper", userId).thenReturn(true);
6.3.3.模拟构造方法
PowerMock提供PowerMockito.whenNew方法来模拟构造方法,但是需要把使用构造方法的类放在@PrepareForTest注解中。
PowerMockito.whenNew(UserDO.class).withNoArguments().thenReturn(userDO);
PowerMockito.whenNew(UserDO.class).withArguments(userId, userName).thenReturn(userDO);
6.3.4.模拟静态方法
PowerMock提供PowerMockito.mockStatic和PowerMockito.spy来模拟静态方法类,然后就可以模拟静态方法了。同样,需要把对应的模拟类添加到@PrepareForTest注解中。
// 模拟对应的类
PowerMockito.mockStatic(HttpHelper.class);
PowerMockito.spy(HttpHelper.class);
// 模拟对应的方法
PowerMockito.when(HttpHelper.httpPost(SERVER_URL)).thenReturn(response);
PowerMockito.doReturn(response).when(HttpHelper.class, "httpPost", SERVER_URL);
PowerMockito.when(HttpHelper.class, "httpPost", SERVER_URL).thenReturn(response);
注意:第一种方式不适用于PowerMockito.spy模拟的静态方法类。
7.调用被测方法
在准备好参数对象后,就可以调用被测试方法了。
如果把方法按访问权限分类,可以简单地分为有访问权限和无访问权限两种。但实际上,Java语言中提供了public、protected、private和缺失共4种权限修饰符,在不同的环境下又对应不同的访问权限。具体映射关系如下:
修饰符 | 本类 | 本包 | 子类 | 其它 |
---|---|---|---|---|
public | 有 | 有 | 有 | 有 |
protected | 有 | 有 | 有 | 无 |
缺省 | 有 | 有 | 无 | 无 |
private | 有 | 无 | 无 | 无 |
下面,将根据有访问权限和无访问权限两种情况,来介绍如何调用被测方法。
7.1.调用构造方法
7.1.1.调用有访问权限的构造方法
可以直接调用有访问权限的构造方法。
UserDO user = new User();
UserDO user = new User(1L, "admin");
7.1.2.调用无访问权限的构造方法
调用无访问权限的构造方法,可以使用PowerMock提供的Whitebox.invokeConstructor方法。
Whitebox.invokeConstructor(NumberHelper.class);
Whitebox.invokeConstructor(User.class, 1L, "admin");
备注:该方法也可以调用有访问权限的构造方法,但是不建议使用。
7.2.调用普通方法
7.2.1.调用有访问权限的普通方法
可以直接调用有访问权限的普通方法。
userService.deleteUser(userId);
User user = userService.getUser(userId);
7.2.2.调用无权限访问的普通方法
调用无访问权限的普通方法,可以使用PowerMock提供的Whitebox.invokeMethod方法。
User user = (User)Whitebox.invokeMethod(userService, "isSuper", userId);
也可以使用PowerMock提供Whitebox.getMethod方法和PowerMockito.method方法,可以直接获取对应类方法对象。然后,通过Method的invoke方法,可以调用没有访问权限的方法。
Method method = Whitebox.getMethod(UserService.class, "isSuper", Long.class);
Method method = PowerMockito.method(UserService.class, "isSuper", Long.class);
User user = (User)method.invoke(userService, userId);
备注:该方法也可以调用有访问权限的普通方法,但是不建议使用。
7.3.调用静态方法
7.3.1.调用有权限访问的静态方法
可以直接调用有访问权限的静态方法。
boolean isPositive = NumberHelper.isPositive(-1);
7.3.2.调用无权限访问的静态方法
调用无权限访问的静态方法,可以使用PowerMock提供的Whitebox.invokeMethod方法。
String value = (String)Whitebox.invokeMethod(JSON.class, "toJSONString", object);
备注:该方法也可以调用有访问权限的静态方法,但是不建议使用。
8.验证依赖方法
在单元测试中,验证是确认模拟的依赖方法是否按照预期被调用或未调用的过程。Mockito提供了许多方法来验证依赖方法调用,给我们编写单元测试用例带来了很大的帮助。
8.1.根据参数验证方法调用
8.1.1.验证无参数方法调用
Mockito.verify(userDAO).deleteAll();
8.1.2.验证指定参数方法调用
Mockito.verify(userDAO).delete(userId);
Mockito.verify(userDAO).delete(Mockito.eq(userId));
8.1.3.验证任意参数方法调用
Mockito.verify(userDAO).delete(Mockito.anyLong());
8.1.4.验证可空参数方法调用
Mockito.verify(userDAO).queryCompany(Mockito.anyLong(), Mockito.nullable(Long.class));
8.1.5.验证必空参数方法调用
Mockito.verify(userDAO).queryCompany(Mockito.anyLong(), Mockito.isNull());
8.1.6.验证可变参数方法调用
对于一些变长度参数方法,可以按实际参数个数进行验证:
Mockito.verify(userService).delete(Mockito.any(Long.class));
Mockito.verify(userService).delete(1L, 2L, 3L);
也可以用Mockito.any()进行通用验证:
Mockito.verify(userService).delete(Mockito.< Long>any());
8.2.验证方法调用次数
8.2.1.验证方法默认调用1次
Mockito.verify(userDAO).delete(userId);
8.2.2.验证方法从不调用
Mockito.verify(userDAO, Mockito.never()).delete(userId);
8.2.3.验证方法调用n次
Mockito.verify(userDAO, Mockito.times(n)).delete(userId);
8.2.4.验证方法调用至少1次
Mockito.verify(userDAO, Mockito.atLeastOnce()).delete(userId);
8.2.5.验证方法调用至少n次
Mockito.verify(userDAO, Mockito.atLeast(n)).delete(userId);
8.2.2.验证方法调用最多1次
Mockito.verify(userDAO, Mockito.atMostOnce()).delete(userId);
8.2.6.验证方法调用最多n次
Mockito.verify(userDAO, Mockito.atMost(n)).delete(userId);
8.2.7.验证方法调用指定n次
Mockito允许按顺序进行验证方法调用,未被验证到的方法调用将不会被标记为已验证。
Mockito.verify(userDAO, Mockito.call(n)).delete(userId);
8.2.8.验证对象及其方法调用1次
用于验证对象及其方法调用1次,如果该对象还有别的方法被调用或者该方法调用了多次,都将导致验证方法调用失败。
Mockito.verify(userDAO, Mockito.only()).delete(userId);
相当于:
Mockito.verify(userDAO).delete(userId);
Mockito.verifyNoMoreInteractions(userDAO);
8.3.验证方法调用并捕获参数值
Mockito提供ArgumentCaptor类来捕获参数值,通过调用forClass(Class< T> clazz)方法来构建一个ArgumentCaptor对象,然后在验证方法调用时来捕获参数,最后获取到捕获的参数值并验证。如果一个方法有多个参数都要捕获并验证,那就需要创建多个ArgumentCaptor对象。
ArgumentCaptor的主要接口方法:
- capture方法,用于捕获方法参数;
- getValue方法,用于获取捕获的参数值,如果捕获了多个参数值,该方法只返回最后一个参数值;
- getAllValues方法,用户获取捕获的所有参数值。
8.3.1.使用ArgumentCaptor.forClass方法定义参数捕获器
在测试用例方法中,直接使用ArgumentCaptor.forClass方法定义参数捕获器。
注意:定义泛型类的参数捕获器时,存在强制类型转化,会引起编译器警告。
8.3.2.使用@Captor注解定义参数捕获器
也可以用Mockito提供的@Captor注解,在测试用例类中定义参数捕获器。
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
@Captor
private ArgumentCaptor< UserDO> userCaptor;
@Test
public void testModifyUser() {
...
Mockito.verify(userDAO).modify(userCaptor.capture());
UserDO user = userCaptor.getValue();
}
}
注意:定义泛型类的参数捕获器时,由于是Mockito自行初始化,不会引起编译器告警。
8.3.3.捕获多次方法调用的参数值列表
8.4.验证其它特殊方法
8.4.1.验证final方法调用
final方法的验证跟普通方法类似,这里不再累述。
8.4.2.验证私有方法调用
PowerMockito提供verifyPrivate方法验证私有方法调用。
PowerMockito.verifyPrivate(myClass, times(1)).invoke("unload", any(List.class));
8.4.3.验证构造方法调用
PowerMockito提供verifyNew方法验证构造方法调用。
PowerMockito.verifyNew(MockClass.class).withNoArguments();
PowerMockito.verifyNew(MockClass.class).withArguments(someArgs);
8.4.4.验证静态方法调用
PowerMockito提供verifyStatic方法验证静态方法调用。
PowerMockito.verifyStatic(StringUtils.class);
StringUtils.isEmpty(string);
9.验证数据对象
JUnit测试框架中Assert类就是断言工具类,主要验证单元测试中实际数据对象与期望数据对象一致。在调用被测方法时,需要对返回值和异常进行验证;在验证方法调用时,也需要对捕获的参数值进行验证。
9.1.验证数据对象空值
9.1.1.验证数据对象为空
通过JUnit提供的Assert.assertNull方法验证数据对象为空。
Assert.assertNull("用户标识必须为空", userId);
9.1.2.验证数据对象非空
通过JUnit提供的Assert.assertNotNull方法验证数据对象非空。
Assert.assertNotNull("用户标识不能为空", userId);
9.2.验证数据对象布尔值
9.2.1.验证数据对象为真
通过JUnit提供的Assert.assertTrue方法验证数据对象为真。
Assert.assertTrue("返回值必须为真", NumberHelper.isPositive(1));
9.2.2.验证数据对象为假
通过JUnit提供的Assert.assertFalse方法验证数据对象为假。
Assert.assertFalse("返回值必须为假", NumberHelper.isPositive(-1));
9.3.验证数据对象引用
在单元测试用例中,对于一些参数或返回值对象,不需要验证对象具体取值,只需要验证对象引用是否一致。
9.3.1.验证数据对象一致
JUnit提供的Assert.assertSame方法验证数据对象一致。
UserDO expectedUser = ...;
Mockito.doReturn(expectedUser).when(userDAO).get(userId);
UserDO actualUser = userService.getUser(userId);
Assert.assertSame("用户必须一致", expectedUser, actualUser);
9.3.1.验证数据对象不一致
JUnit提供的Assert.assertNotSame方法验证数据对象一致。
UserDO expectedUser = ...;
Mockito.doReturn(expectedUser).when(userDAO).get(userId);
UserDO actualUser = userService.getUser(otherUserId);
Assert.assertNotSame("用户不能一致", expectedUser, actualUser);
9.4.验证数据对象值
JUnit提供Assert.assertEquals、Assert.assertNotEquals、Assert.assertArrayEquals方法组,可以用来验证数据对象值是否相等。
9.4.1.验证简单数据对象
对于简单数据对象(比如:基础类型、包装类型、实现了equals的数据类型……),可以直接通过JUnit的Assert.assertEquals和Assert.assertNotEquals方法组进行验证。
Assert.assertNotEquals("用户名称不一致", "admin", userName);
Assert.assertEquals("账户金额不一致", 10000.0D, accountAmount, 1E-6D);
9.4.2.验证简单数组或集合对象
对于简单数组对象(比如:基础类型、包装类型、实现了equals的数据类型……),可以直接通过JUnit的Assert.assertArrayEquals方法组进行验证。对于简单集合对象,也可以通过Assert.assertEquals方法验证。
Long[] userIds = ...;
Assert.assertArrayEquals("用户标识列表不一致", new Long[] {1L, 2L, 3L}, userIds);
List< Long> userIdList = ...;
Assert.assertEquals("用户标识列表不一致", Arrays.asList(1L, 2L, 3L), userIdList);
9.4.3.验证复杂数据对象
对于复杂的JavaBean数据对象,需要验证JavaBean数据对象的每一个属性字段。
UserDO user = ...;
Assert.assertEquals("用户标识不一致", Long.valueOf(1L), user.getId());
Assert.assertEquals("用户名称不一致", "admin", user.getName());
Assert.assertEquals("用户公司标识不一致", Long.valueOf(1L), user.getCompany().getId());
...
9.4.4.验证复杂数组或集合对象
对于复杂的JavaBean数组和集合对象,需要先展开数组和集合对象中每一个JavaBean数据对象,然后验证JavaBean数据对象的每一个属性字段。
List< UserDO> expectedUserList = ...;
List< UserDO> actualUserList = ...;
Assert.assertEquals("用户列表长度不一致", expectedUserList.size(), actualUserList.size());
UserDO[] expectedUsers = expectedUserList.toArray(new UserDO[0]);
UserDO[] actualUsers = actualUserList.toArray(new UserDO[0]);
for (int i = 0; i < actualUsers.length; i++) {
Assert.assertEquals(String.format("用户(%s)标识不一致", i), expectedUsers[i].getId(), actualUsers[i].getId());
Assert.assertEquals(String.format("用户(%s)名称不一致", i), expectedUsers[i].getName(), actualUsers[i].getName());
Assert.assertEquals("用户公司标识不一致", expectedUsers[i].getCompany().getId(), actualUsers[i].getCompany().getId());
...
}
9.4.5.通过序列化验证数据对象
如上一节例子所示,当数据对象过于复杂时,如果采用Assert.assertEquals依次验证每个JavaBean对象、验证每一个属性字段,测试用例的代码量将会非常庞大。这里,推荐使用序列化手段简化数据对象的验证,比如利用JSON.toJSONString方法把复杂的数据对象转化为字符串,然后再使用Assert.assertEquals方法进行验证字符串。但是,序列化值必须具备有序性、一致性和可读性。
List< UserDO> userList = ...;
String text = ResourceHelper.getResourceAsString(getClass(), "userList.json");
Assert.assertEquals("用户列表不一致", text, JSON.toJSONString(userList));
通常使用JSON.toJSONString方法把Map对象转化为字符串,其中key-value的顺序具有不确定性,无法用于验证两个对象是否一致。这里,JSON提供序列化选项SerializerFeature.MapSortField(映射排序字段),可以用于保证序列化后的key-value的有序性。
Map< Long, Map< String, Object>> userMap =
上一篇: 单元测试