您现在的位置是:首页 >其他 >Java单元测试实战(二)编写流程网站首页其他

Java单元测试实战(二)编写流程

SJMP1974 2023-05-28 16:00:03
简介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. 一个有依赖的单元测试
  • 定义对象:定义测试对象,模拟依赖对象、注入依赖对象;
  • 模拟方法:模拟参数或返回值、模拟依赖方法;
  • 调用方法:传入参数对象、调用测试方法、验证返回值或异常;
  • 验证方法:验证依赖方法、验证方法参数、验证依赖对象。

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

/**
* 用户服务类
*/
@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:
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

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。