赞
踩
版权声明:本文为博主「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 功能,用于拦截那些尚未实现或不期望被真实调用的方法,
默认所有方法都是真实方法,除非主动去模拟对应方法。所以,利用spy 功能来定
义被测对象,适合于需要模拟被测类自身方法的情况,适用于普通类、接口和虚基
类。
@Spy注解跟Mockito.spy 方法一样,可以用来定义被测对象,适合于需要模拟被测类自身方法的情况,适用于普通类、接口和虚基类。@Spy注解需要配合@RunWith注解使用。
@InjectMocks 注解用来创建一个实例,并将其它对象(@Mock、@Spy或直接定义的对象)注入到该实例中。所以,@InjectMocks 注解本身就可以用来定义被测对象。@InjectMocks 注解需要配合@RunWith 注解使用。
在编写单元测试用例时,需要模拟各种依赖对象——类成员、方法参数和方法返回值。
如果需要构建一个对象,最简单直接的方法就是——定义对象并赋值。
如果对象字段或层级非常庞大,采用直接构建对象方法,可能会编写大量构建程序代码。这种情况,可以考虑反序列化对象,将会大大减少程序代码。由于JSON 字符串可读性高,这里就以JSON 为例,介绍反序列化对象。
反序列化模型对象:
反序列化模型对象:
反序列化映射对象:
Mockito 默认所有方法都已被模拟——方法为空并返回默认值(null 或0),除非主动执行doCallRealMethod 或thenCallRealMethod 操作,才能够调用真实的方法。
@Mock 注解跟 Mockito.mock 方法一样,可以用来模拟依赖对象,适用于普通类、接口和虚基类。@Mock 注解需要配合 @RunWith 注解使用。
@Spy注解跟Mockito.spy 方法一样,可以用来模拟依赖对象,适用于普通类、接口和虚基类。@Spy 注解需要配合@RunWith 注解使用。
@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)。
使用@Captor 注解定义参数捕获器
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 方法清除上一次的方法调用。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。