Java单元测试实践-00.目录(9万多字文档+700多测试示例)
https://blog.csdn.net/a82514921/article/details/107969340
1. Mybatis与Mock
在对Mybatis的Mapper对象进行处理时,可能需要使某个Mapper对象在某些情况下返回指定值,在某些情况下执行真实方法;或者需要对Mapper对象相关的数据库操作进行记录,以下进行说明。
1.1. 测试示例说明
与Mybatis相关的测试类在测试示例的adrninistrator.test.testmock.mybatis包中,该包中的测试代码执行时需要连接MySQL数据库,由于示例配置文件中默认使用MockDriver作为数据源驱动,不会连接MySQL数据库,因此以上包中的测试代码不能直接执行。
在执行adrninistrator.test.testmock.mybatis包中的测试类时,可以使用gradlew/gradle命令,并指定“use_mysql”参数,unittest.gradle脚本在发现执行Gradle任务包含“use_mysql”参数时,会执行以上包中的测试类,并使用base_MySQL.properties配置文件覆盖base.properties,从而使测试程序连接MySQL;不包含以上参数时,不会执行以上包中的测试类。
使用gradle命令,并指定“use_mysql”参数的命令示例如下:
gradlew/gradle test -Puse_mysql
在进行以上测试时,需要确保base_MySQL.properties配置文件中的数据库参数对应的数据库服务可连接,并执行create-table.sql文件中的SQL语句创建测试代码使用的测试数据库表。
1.2. Mapper对象类名
Mybatis的Mapper对象使用了JDK动态代理,类名示例为“com.sun.proxy.$Proxy44”。可参考示例TestMybatisMapperInfo类。
1.3. 对Mapper对象进行Mock
1.3.1. 修改Mapper对象的Mock对象的返回值/抛出异常
对Mapper对象的Mock对象,可以进行Stub,修改指定方法的行为,修改返回值或抛出异常,示例如下:
TestTableMapper testTableMapper = Mockito.mock(TestTableMapper.class);
Mockito.when(testTableMapper.selectByPrimaryKey(Mockito.anyString())).thenReturn(new TestTableEntity());
可参考示例TestMockMybatisMapperReturn1、TestMockMybatisMapperReturn2、TestMockMybatisMapperThrows类。
1.3.2. 使Mapper对象的Mock对象执行真实方法
执行Mockito.mock()方法创建Mapper对象的Mock对象时,指定默认Answer为执行真实方法,执行Mock对象的方法时,不会执行真实方法。可参考示例TestMockMybatisMapperNotCallRealMethod类。
可对Mapper对象的Mock对象进行Stub,当请求参数满足条件时执行真实方法,可以在Mapper对象的Mock对象的Answer中执行原始的Mapper对象的方法,从而执行真实方法。示例如下:
@Autowired
private TestTableMapper testTableMapper;
String time = String.valueOf(System.currentTimeMillis());
TestTableMapper testTableMapperMock = Mockito.mock(TestTableMapper.class);
Mockito.when(testTableMapperMock.insert(
Mockito.argThat(argument -> time.equals(argument.getId()) && time.equals(argument.getFlag()))))
.thenAnswer(invocation -> {
TestTableEntity arg1 = invocation.getArgument(0);
return testTableMapper.insert(arg1);
});
Mockito.when(testTableMapperMock.selectByPrimaryKey(time)).thenAnswer(invocation -> {
String arg1 = invocation.getArgument(0);
return testTableMapper.selectByPrimaryKey(arg1);
});
可参考示例TestMockMybatisMapperCallRealMethod类。
1.4. 对Mapper对象进行Spy
1.4.1. Mapper对象不支持Spy操作
Mapper对象不支持Spy操作,对Mapper对象执行Mockito.spy操作时,会出现异常,示例及异常信息如下所示。
@Autowired
private TestTableMapper testTableMapper;
assertThrows(Exception.class, () ->
Mockito.spy(testTableMapper)
);
org.mockito.exceptions.base.MockitoException
Cannot mock/spy class com.sun.proxy.$Proxy44
Mockito cannot mock/spy because :
- final class
1.4.2. 对MapperProxy进行Spy
在Mapper对象中,包含类型为MapperProxy<T>(T为当前Mapper对象对应的接口),名称为“h”的对象。
org.apache.ibatis.binding.MapperProxy类在mybatis-xxx.jar中,当Mybatis进行数据库操作时,会执行MapperProxy.invoke()方法。
MapperProxy类中包含类型为Class<T>的变量mapperInterface,即MapperProxy类对应的Mapper对象的类型。
1.4.2.1. 对MapperProxy进行Spy的过程
可对MapperProxy对象进行Spy操作,再将Mapper对象中的MapperProxy替换为Spy对象,示例如下:
MapperProxy<TestTableMapper> mapperProxy = Whitebox.getInternalState(testTableMapper, "h");
proxySpy = Mockito.spy(mapperProxy);
Whitebox.setInternalState(testTableMapper, proxySpy);
以上处理过程可参考示例TestSpyMybatisMapperBase类。
1.4.2.2. MapperProxy.invoke()方法调用参数
MapperProxy.invoke()方法的参数为“Object proxy, Method method, Object[] args”。
对MapperProxy.invoke()方法进行Stub,在Answer中获取到的参数内容如下:
参数 | 类型 | 说明 |
---|---|---|
参数1 | Object对象 | 被调用的Mapper对象 |
参数2 | Method对象 | 被调用的Mapper对象的方法 |
参数3 | Object数组 | 调用Mapper对象的方法时传入的参数 |
通过invocation对象的相关方法可以获取上述参数,示例如下:
PowerMockito.doAnswer(invocation -> {
Object object = invocation.getArgument(0);
Method method = invocation.getArgument(1);
Object[] objects = invocation.getArgument(2);
// 参数1为Mapper对象
assertEquals(testTableMapper.getClass(), object.getClass());
// 参数2为被调用的Mapper对象的方法
assertEquals("selectByPrimaryKey", method.getName());
// 参数3为调用Mapper对象的方法时传入的参数
assertEquals(1, objects.length);
return ...;
}).when(proxySpy).invoke(Mockito.any(Object.class), Mockito.any(Method.class), Mockito.any(Object[].class));
可参考示例TestSpyMybatisMapperGetArgs类。
1.4.2.3. 支持的Stub操作
获取Mapper对象中的MapperProxy对象后,对其Spy对象的方法进行Stub,可在Answer中通过invocation.callRealMethod()执行真实方法,示例如下:
PowerMockito.doAnswer(invocation -> invocation.callRealMethod()).when(proxySpy).invoke(Mockito.any(Object
.class), Mockito.any(Method.class), Mockito.any(Object[].class));
可参考示例TestSpyMybatisMapperCallRealMethod类。
还可在Answer中对调用参数进行检查。可参考示例TestSpyMybatisMapperCheckArgs类。
1.5. 对MapperProxy类的invoke()方法进行Replace
Mapper对象中的MapperProxy类的方法支持进行Replace处理。需要使用@PrepareForTest注解指定MapperProxy.class。
对MapperProxy.invoke()方法进行Replace,PowerMockito.replace().with()方法指定的InvocationHandler实例的参数为“Object proxy, Method method, Object[] args”,以上参数的说明如下。
参数 | 类型 | 说明 |
---|---|---|
参数1 | Object对象 | MapperProxy对象 |
参数2 | Method对象 | 被调用的MapperProxy对象的invoke方法 |
参数3 | Object数组 | 与前文表格的参数说明相同 |
MapperProxy类中包含的变量mapperInterface,等于MapperProxy类对应的Mapper类型,示例如下:
PowerMockito.replace(PowerMockito.method(MapperProxy.class, "invoke")).with((proxy, method, args) -> {
// 参数1为MapperProxy
assertTrue(proxy instanceof MapperProxy);
MapperProxy mapperProxy = (MapperProxy) proxy;
Class<Object> mapperInterfaceClass = Whitebox.getInternalState(mapperProxy, "mapperInterface");
// 参数2为invoke方法
assertTrue(method instanceof Method);
assertEquals("invoke", method.getName());
// 参数3为调用参数列表
assertEquals(3, args.length);
// args的参数1为被调用的Mapper对象
assertEquals(testTableMapper.getClass(), args[0].getClass());
// args的参数2为被调用的Mapper对象的方法
assertTrue(args[1] instanceof Method);
Method method1 = (Method) args[1];
// args的参数3为调用Mapper对象的方法时传入的参数列表
assertTrue(args[2] instanceof Object[]);
Object[] args2 = (Object[]) args[2];
assertEquals(1, args2.length);
return ...;
});
可参考示例TestReplaceMybatisMapperGetArgs类。
在PowerMockito.replace().with()方法指定的InvocationHandler实例中,可以根据调用参数决定执行真实方法(执行method.invoke()方法)或返回指定值,示例如下:
PowerMockito.replace(PowerMockito.method(MapperProxy.class, "invoke")).with((proxy, method, args) -> {
MapperProxy mapperProxy = (MapperProxy) proxy;
Class<Object> mapperInterfaceClass = Whitebox.getInternalState(mapperProxy, "mapperInterface");
// 当不是TestTableMapper时,执行真实方法
if (!TestTableMapper.class.equals(mapperInterfaceClass)) {
return method.invoke(proxy, args);
}
// 其他情况返回指定值
TestTableEntity testTableEntity = new TestTableEntity();
...
return testTableEntity;
});
可参考示例TestReplaceMybatisMapperCallRealMethod类。
1.6. 对Mapper对象委托方法调用
将Mapper对象替换为方法调用被委托的Mock对象,可执行真实方法或返回指定值。
对Mapper对象委托方法调用可参考示例TestMockMybatisMapperDelegatesTo1、TestMybatisDelegate类,示例如下:
TestMybatisDelegate类为Spring的@Component组件,在test模块中(需要确保添加至Spring的包扫描路径中),可当作TestTableMapper的被委托代表使用,与TestTableMapper具有相同的方法,在TestMybatisDelegate中注入了TestTableMapper对象,可以根据需要执行真实方法,或返回指定的值,如下所示:
@Service
public class TestMybatisDelegate {
@Autowired
private TestTableMapper testTableMapper;
private String time;
public int deleteByPrimaryKey(String id) {
return 0;
}
public int insert(TestTableEntity record) {
if (record == null) {
return 0;
}
if (time.equals(record.getId()) && time.equals(record.getFlag())) {
return testTableMapper.insert(record);
}
return 0;
}
...
public void setTime(String time) {
this.time = time;
}
}
在TestMockMybatisMapperDelegatesTo1类中,注入了TestTableMapper的被委托代表TestMybatisDelegate的实例,在生成TestTableMapper的Mock对象时,使用TestMybatisDelegate实例作为其被委托代表,并将TestService3对象中的TestTableMapper对象替换为TestMybatisDelegate实例,如下所示:
@Autowired
private TestService3 testService3;
@Autowired
private TestMybatisDelegate testMybatisDelegate;
TestTableMapper testTableMapperMock = Mockito.mock(TestTableMapper.class, AdditionalAnswers.delegatesTo(testMybatisDelegate));
Whitebox.setInternalState(testService3, testTableMapperMock);
1.7. 在test模块使用mybatis-generator
在使用mybatis-generator时,可将对mybatis-generator的依赖添加至test模块中,在执行mybatis-generator时,指定test模块。
例如使用Gradle时添加至testImplementation中,可以避免添加至main模块后打包时包含mybatis-generator。
1.8. 在测试代码中使用Mybatis Mapper对象
可在测试代码中注入并使用Mybatis Mapper对象,可以直接测试Mybatis Mapper对象的方法,或者通过Mapper对象在测试开始前生成测试数据,或在测试结束后查询被测试代码生成的数据库记录是否符合预期。