最近系统需要引进单元测试Mock框架,也就是在进行单元测试的时候,跳过RPC调用,直接通过Mock方式返回。然后就进行一个简单的调研,通应用到系统中。
一.Mock框架调研
1.1.系统诉求
- 提高单元测试质量
- 提高单测行覆盖率
- mock代替RPC调用
1.2.调研分析
通过调研和查阅文档,对于市面上几款单元测试框架进行了比对分析,其结果如下:
框架 | 优点 | 缺点 | 备注 |
---|---|---|---|
小黄瓜 | 自动化测试佼佼者,可直接编写解析纯文本 | 上手难度大,系统额外侵入文件。 | |
Junit | 单元测试利器,使用方便 | 无法解决mock问题。 | |
JMock | mock接口的支持度较好 | 编写mock对象,需要显示指定,方法模拟较为复杂。 | |
EasyMock | 对于快速mock数据较为简便 | 行业推荐较少 | |
Mockito | 快速上手,代码简洁,并且异常信息更为清晰,相比api更简单,更加面向对象。 | 对于较为复杂的调用,mock起来比较复杂 | 推荐 |
PowerMock | 兼EasyMock与Mockito的优点 | 可能存在jcoco,sonar不兼容问题。 | 推荐 |
1.3采用框架
和团队讨论决定:使用Mockito
二.Mock框架引入
2.1.Mockito介绍
一款优秀的单元测试框架,能够支持mock创建,验证结果,实现打桩(stubbing)
关于具体详细介绍不做赘述,详情参考官方文档:官网
2.2.项目使用
- 引入Maven依赖
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core --> select采用的版本
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.10.19</version>
</dependency>
- 创建Mock注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD })
@Documented
public @interface MockTest {//仅仅是作为mock接口的标记
}
- mock监听器
public class MockitoBeansTestExecutionListener extends DependencyInjectionTestExecutionListener {
//声明mock实例容器
private static Map<Class<?>, MockBeanWrapper> mockBeans = new ConcurrentHashMap<>();
private static Map<Class<?>, List<Field>> injectMockBeans = new ConcurrentHashMap<>();
private static boolean hasInitialized = false;
public static Map<Class<?>, MockBeanWrapper> resolvedAllMockBeans() {
Assert.isTrue(hasInitialized);
return Collections.unmodifiableMap(mockBeans);
}
@Override
public void beforeTestMethod(TestContext testContext) throws Exception {
//获取对象实例
Object testInstance = testContext.getTestInstance();
//获取注入实例对象
List<Field> fields = injectMockBeans.get(testContext.getTestClass());
//设置访问权限,并添加mock对象
if (CollectionUtils.isNotEmpty(fields)){
for (Field field : fields){
field.setAccessible(true);
field.set(testInstance,mockBeans.get(field.getType()).getMockObject());
}
}
}
@Override
public void beforeTestClass(TestContext testContext) throws Exception {
//获取当前MockBean数据,如果父类中有MockBean则将其加入
Field[] fields = testContext.getTestClass().getDeclaredFields();
Field[] superFields = testContext.getTestClass().getSuperclass().getDeclaredFields();
List<Field> mockFields = Lists.newArrayList(fields);
if (superFields.length > 0){
mockFields.addAll(Lists.newArrayList(superFields));
}
//将需要mock的对象创建出来
for (Field field : mockFields) {
MockTest mockTest = field.getAnnotation(MockTest.class);
if (mockTest != null) {
MockBeanWrapper wrapper = new MockBeanWrapper();
Class<?> type = field.getType();
wrapper.setMockObject(Mockito.mock(type));
wrapper.setBeanType(type);
wrapper.setBeanName(field.getName());
mockBeans.putIfAbsent(wrapper.getBeanType(), wrapper);
injectMockBeans.compute(testContext.getTestClass(), (targetClass, waitInjectFields) -> {
if (waitInjectFields == null) {
waitInjectFields = new ArrayList<>();
}
waitInjectFields.add(field);
return waitInjectFields;
});
}
}
hasInitialized = true;
}
@Data
public class MockBeanWrapper {
private String beanName;
private Class<?> beanType;
private Object mockObject;
}
}
- 注册到Bean工厂
public class MockitoBeansPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
Map<Class<?>, MockitoBeansTestExecutionListener.MockBeanWrapper> allMockBeans = MockitoBeansTestExecutionListener.resolvedAllMockBeans();
for (Map.Entry<Class<?>, MockitoBeansTestExecutionListener.MockBeanWrapper> mockBeanWrapperEntry : allMockBeans.entrySet()) {
beanFactory.registerResolvableDependency(mockBeanWrapperEntry.getKey(), mockBeanWrapperEntry.getValue().getMockObject());
beanFactory.registerSingleton(mockBeanWrapperEntry.getValue().getBeanName(), mockBeanWrapperEntry.getValue().getMockObject());
}
}
}
- 实际使用案例
- 启动类改造
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:context-test.xml"})
@TestExecutionListeners({MockitoBeansTestExecutionListener.class,
TransactionalTestExecutionListener.class,
SqlScriptsTestExecutionListener.class})
@Transactional(rollbackFor = Exception.class)
public class TestBase extends JUnit4SpringContextTests {
//公共处理逻辑
}
- 单元测试使用
public class MaxBrandAdapterTest extends TestBase {
@Resource
private MaxBrandAdapter maxBrandAdapter;
@MockTest
MaxBrandApi maxBrandApi;
@Test
public void queryMaxBrandExceptionTest() {
//Mock异常场景
Mockito.when(maxBrandApi.queryHotBrand(Mockito.any(),Mockito.anyLong(),Mockito.anyString())).thenThrow(Exception.class);
List<Long> list = maxBrandAdapter.queryMaxBrandByUserId("userId");
if (CollectionUtils.isNotEmpty(list)) {
for (Long id : list) {
System.out.println(id);
}
}
Assert.assertNull(list);
}
@Test
public void queryHotBrandTest(){
//mock正常场景
Mockito.when(maxBrandApi.queryHotBrand(Mockito.any(),Mockito.anyLong(),Mockito.anyString()))
.thenReturn(mockResultData());
List<Long> list = maxBrandAdapter.queryMaxBrandByUserId("userId");
if (CollectionUtils.isNotEmpty(list)) {
for (Long id : list) {
System.out.println(id);
}
}
Assert.assertNotNull(list);
}
//mock结果数据
private MaxResult<MaxBrandTO> mockResultData(){
MaxBrandTO brandTO = new MaxBrandTO();
brandTO.setBrandIds("123,345,123");
MaxResult<HotBrandTO> result = new MaxResult();
result.success();
result.setData(brandTO);
return result;
}
}