0


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 文件中加入
以下包依赖:
image.png

其中,powermock.version 为2.0.9,为当前的最新版本,可根据实际情况修改。在
PowerMock包中,已经包含了对应的Mockito和JUnit包,所以无需单独引入Mockito
和JUnit 包。

单元测试案例***

  1. 一个有依赖的单元测试
  • 定义对象:定义测试对象,模拟依赖对象、注入依赖对象;
  • 模拟方法:模拟参数或返回值、模拟依赖方法;
  • 调用方法:传入参数对象、调用测试方法、验证返回值或异常;
  • 验证方法:验证依赖方法、验证方法参数、验证依赖对象。

业务代码(可以先大致看下):

/**
* 用户服务类
*/@ServicepublicclassUserService{/** 定义依赖对象 *//** 用户DAO */@AutowiredprivateUserDAO userDAO;/** 标识生成器 */@AutowiredprivateIdGenerator idGenerator;/** 定义依赖参数 *//** 可以修改 */@Value("${userService.canModify}")privateBoolean canModify;/**
* 保存用户
*
* @param userSave 用户保存
* @return 用户标识
*/publicLongsaveUser(UserVO userSave){// 获取用户标识Long userId = userDAO.getIdByName(userSave.getName());// 根据存在处理// 根据存在处理: 不存在则创建if(Objects.isNull(userId)){
            userId = idGenerator.next();UserDO userCreate =newUserDO();
            userCreate.setId(userId);
            userCreate.setName(userSave.getName());
            userCreate.setDescription(userSave.getDescription());
            userDAO.create(userCreate);}// 根据存在处理: 已存在可修改elseif(Boolean.TRUE.equals(canModify)){UserDO userModify =newUserDO();
            userModify.setId(userId);
            userModify.setName(userSave.getName());
            userModify.setDescription(userSave.getDescription());
            userDAO.modify(userModify);}// 根据存在处理: 已存在禁修改else{thrownewUnsupportedOperationException("不支持修改");}// 返回用户标识return userId;}}

对应的单元测试用例:
代码中的第一个单元测试方法重点看!!!

/**
* 用户服务测试类
* 
*/@RunWith(MockitoJUnitRunner.class)publicclassUserServiceTest{/** 1.1 定义测试对象 *//** 1.3 InjectMocks 和 Mock 配合可以将 userDAO 注入 userService
 用户服务 */@InjectMocksprivateUserService userService;/** 1.2 模拟依赖对象 *//** 用户DAO */@MockprivateUserDAO  userDAO;/** 定义静态常量 *//** 资源路径 */privatestaticfinalString RESOURCE_PATH ="testUserService/";/** 标识生成器 */@MockprivateIdGenerator idGenerator;/**
    * 在测试之前
    */@BeforepublicvoidbeforeTest(){Whitebox.setInternalState(userService,"canModify",Boolean.TRUE);}/**
    * 测试: 保存用户-创建
    */@TestpublicvoidtestSaveUserWithCreate(){// 2.1 模拟依赖方法// 模拟依赖方法: userDAO.getIdByNameMockito.doReturn(null).when(userDAO).getIdByName(Mockito.anyStri
                                                         ng());// 2.1 模拟依赖方法: idGenerator.nextLong 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.getIdByNameMockito.verify(userDAO).getIdByName(userSave.getName());// 4.1 验证依赖方法: idGenerator.nextMockito.verify(idGenerator).next();// 4.2  验证方法参数: userDAO.createArgumentCaptor<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);}/**
    * 测试: 保存用户-修改
    */@TestpublicvoidtestSaveUserWithModify(){// 模拟依赖方法// 模拟依赖方法: userDAO.getIdByNameLong 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.getIdByNameMockito.verify(userDAO).getIdByName(userSave.getName());// 验证依赖对象Mockito.verifyNoMoreInteractions(userDAO, idGenerator);}}

其中,加载的JSON 资源文件内容如下:
userSave.json:
image.png

userCreate.json:
image.png

userModify.json:
image.png

通过执行以上测试用例,可以看到对源代码进行了100%的行覆盖。

小结:
编写测试用例流程如下:
image.png
如上图所示,第1、3 步适用于大多数单元测试,而第2、4 步只适用于有外部依赖的单元测试。

下面将对各个细节进行拆解:

流程详细介绍

定义被测对象

  1. 直接构建对象,简单直接

image.png

  1. 利用Mockito.spy 方法

Mockito 提供一个spy 功能,用于拦截那些尚未实现或不期望被真实调用的方法,
默认所有方法都是真实方法,除非主动去模拟对应方法。所以,利用spy 功能来定
义被测对象,适合于需要模拟被测类自身方法的情况,适用于普通类、接口和虚基
类。
image.png

  1. 利用 @Spy 注解

@Spy注解跟Mockito.spy 方法一样,可以用来定义被测对象,适合于需要模拟被测类自身方法的情况,适用于普通类、接口和虚基类。@Spy注解需要配合@RunWith注解使用。
image.png

  1. 利用@InjectMocks 注解

@InjectMocks 注解用来创建一个实例,并将其它对象(@Mock、@Spy或直接定义的对象)注入到该实例中。所以,@InjectMocks 注解本身就可以用来定义被测对象。@InjectMocks 注解需要配合@RunWith 注解使用。
image.png

模拟依赖对象

在编写单元测试用例时,需要模拟各种依赖对象——类成员、方法参数和方法返回值。

  1. 直接构建对象

如果需要构建一个对象,最简单直接的方法就是——定义对象并赋值。
image.png

  1. 反序列化对象

如果对象字段或层级非常庞大,采用直接构建对象方法,可能会编写大量构建程序代码。这种情况,可以考虑反序列化对象,将会大大减少程序代码。由于JSON 字符串可读性高,这里就以JSON 为例,介绍反序列化对象。

反序列化模型对象:
image.png

反序列化模型对象:
image.png

反序列化映射对象:
image.png

  1. 利用 Mockito.mock 方法 Mockito

Mockito 默认所有方法都已被模拟——方法为空并返回默认值(null 或0),除非主动执行doCallRealMethod 或thenCallRealMethod 操作,才能够调用真实的方法。

  1. 利用@Mock 注解

@Mock 注解跟 Mockito.mock 方法一样,可以用来模拟依赖对象,适用于普通类、接口和虚基类。@Mock 注解需要配合 @RunWith 注解使用。
image.png

  1. 利用Mockito.spy 方法

image.png

  1. 利用@Spy 注解

@Spy注解跟Mockito.spy 方法一样,可以用来模拟依赖对象,适用于普通类、接口和虚基类。@Spy 注解需要配合@RunWith 注解使用。

image.png

注入依赖对象

  1. 利用Setter 方法注入
  2. 利用ReflectionTestUtils.setField 方法注入

image.png

  1. 利用Whitebox.setInternalState 方法注入

image.png

  1. 利用@InjectMocks 注解注入

@InjectMocks 注解用来创建一个实例,并将其它对象(@Mock、@Spy或直接定义的对象)注入到该实例中。@InjectMocks 注解需要配合@RunWith 注解使用。
image.png

  1. 设置静态常量字段值

Whitebox.setInternalState 方法和@InjectMocks 注解并不支持设置静态常量,需要自己实现一个设置静态常量的方法:
image.png
具体使用方法如下:
image.png

注意:经过测试,该方法对于int、Integer 等基础类型并不生效,应该是编译器常量优化导致。

模拟依赖对象

  1. 根据返回模拟方法

image.png

  1. 模拟方法单个返回值

image.png

  1. 模拟方法定制返回值

image.png

  1. 模拟方法抛出单个异常

image.png

  1. 直接调用真实方法

image.png
其他省略…
编写时,未知的细节可以查阅手册

调用被测方法

  1. 调用无权限访问的普通方法

调用无访问权限的普通方法,可以使用PowerMock提供的Whitebox.invokeMethod方法。

image.png

  1. 调用无权限访问的静态方法

image.png

验证依赖方法

在单元测试中,验证是确认模拟的依赖方法是否按照预期被调用或未调用的过程。

  1. 验证无参数方法调用

验证方法默认调用1次
image.png

  1. 验证指定参数方法调用

image.png

  1. 验证任意参数方法调用

在验证依赖方法时,有时候并不关心传入参数的具体值,可以使用Mockito 参数匹配器的any 方法。Mockito 提供了anyInt、anyLong、anyString、anyList、anySet、anyMap、any(Class clazz)等方法来表示任意值。
image.png

  1. 验证必空参数方法调用

同样,如果要匹配null 对象,可以使用isNull 方法,或使用eq(null)。
image.png

  1. 验证方法调用n次

image.png

  1. 验证方法调用至少1次

image.png

  1. 使用ArgumentCaptor.forClass方法定义参数捕获器

image.png

使用@Captor 注解定义参数捕获器

image.png

验证数据对象

  1. 通过JUnit 提供的Assert.assertNull 方法验证数据对象为空。

image.png

  1. 验证数据对象值

JUnit 提供Assert.assertEquals、Assert.assertNotEquals、Assert.assertArrayEquals方法组,可以用来验证数据对象值是否相等。

  1. 验证复杂数组或集合对象

对于复杂的JavaBean 数组和集合对象,需要先展开数组和集合对象中每一个JavaBean 数据对象,然后验证JavaBean 数据对象的每一个属性字段。
image.png

  1. 通过序列化验证数据对象

如上一节例子所示,当数据对象过于复杂时,如果采用Assert.assertEquals 依次验证每个JavaBean 对象、验证每一个属性字段,测试用例的代码量将会非常庞大。这里,推荐使用序列化手段简化数据对象的验证,比如利用JSON.toJSONString 方法把复杂的数据对象转化为字符串,然后再使用Assert.assertEquals 方法进行验证字符串。但是,序列化值必须具备有序性、一致性和可读性。
image.png

通常使用JSON.toJSONString 方法把Map 对象转化为字符串,其中key-value 的顺序具有不确定性,无法用于验证两个对象是否一致。这里,JSON 提供序列化选项SerializerFeature.MapSortField(映射排序字段),可以用于保证序列化后的keyvalue的有序性。
image.png

  1. 验证数据对象私有属性字段

有时候,单元测试用例需要对复杂对象的私有属性字段进行验证。而PowerMockito提供的Whitebox.getInternalState 方法,获取轻松地获取到私有属性字段值。
image.png

验证依赖对象

  1. 验证模拟对象没有任何方法调用

Mockito 提供了verifyNoInteractions 方法,可以验证模拟对象在被测试方法中没有任何调用。
image.png

  1. 验证模拟对象没有更多方法调用

Mockito 提供了verifyNoMoreInteractions 方法,在验证模拟对象所有方法调用后使用,可以验证模拟对象所有方法调用是否都得到验证。如果模拟对象存在任何未验证的方法调用,就会抛出NoInteractionsWanted 异常。

  1. 清除模拟对象所有方法调用标记

在编写单元测试用例时,为了减少单元测试用例数和代码量,可以把多组参数定义在同一个单元测试用例中,然后用for 循环依次执行每一组参数的被测方法调用。为了避免上一次测试的方法调用影响下一次测试的方法调用验证,最好使用Mockito 提供clearInvocations 方法清除上一次的方法调用。
image.png

标签: 单元测试 java junit

本文转载自: https://blog.csdn.net/qq_36188127/article/details/130256559
版权归原作者 SJMP1974 所有, 如有侵权,请联系我们删除。

“Java单元测试实战(二)编写流程”的评论:

还没有评论