真正意义上的spring环境中的单元测试方案spring-test与mokito完美结合
一.要解决的问题:
spring环境中单元测试其实不是真正意义上的单元测试,真正意义上的单元测试应该是隔离所有的依赖,对当前业务实现业务逻辑测试;但是目前spring好像还没提供这样的解决方案,只能做依赖于环境的集成测试。比如:要测试A类,但是A类依赖B类和C类,这个时候我们必须保证B和C是完整的且是相对稳定的没太多bug的类.但是实际开发过程中,C类和B类可能是对数据库操作的Dao层或是对外接口层,这个时候我们在测试A类的时候业务B和C的环境或B或C都现在还没开发完成只是一个接口定义完成,这个时候就很难完成我们A类的测试了。
二.解决方案:
为了解决这个问题我们必须在测试的时候忽略B和C类,换句话说就是假象B和C都是可以运行或按我们预期返回结果的运行。我们利用mockito来掩饰我们测试类的所有的依赖。这样我们需要做到两点1.我们可以让B和C可以控制返回预期;2.B和C必须注入到spring中替换我们的测试类的依赖.
DependencyInjectionAsMockitoTestExecutionListener类是在spring-test中的DependencyInjectionTestExecutionListener基础上扩展的一个结合mock的测试监听器。我们在测试的时候可以用注解TestExecutionListeners指定这个监听器来实现单元测试package org.springframework.test.context.support;import java.lang.annotation.Annotation;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import java.util.ArrayList;import java.util.List;import static org.mockito.Mockito.mock;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.context.TestContext;/** * @author yanglin lv */public class DependencyInjectionAsMockitoTestExecutionListener extends DependencyInjectionTestExecutionListener { private static String SETTER = "set"; private static String GETTER = "get"; @Override protected void injectDependencies(final TestContext testContext) throws Exception { super.injectDependencies(testContext); Object bean = testContext.getTestInstance(); Class[] mockClass = getMockClass(bean.getClass()); Method[] methods = bean.getClass().getDeclaredMethods(); Class clz = bean.getClass(); Object instance = null; List<MockObjectMap> objs = new ArrayList<MockObjectMap>(); autowireMockBean(clz, bean, objs); List<Object> stubObjs = getStubInstance(clz, bean); autowireMockBeanForSpring(stubObjs, objs); } private void autowireMockBeanForSpring(List<Object> stubObjs, List<MockObjectMap> objs) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { for (Object object : stubObjs) { Class claz = object.getClass(); do { for (Method method : claz.getDeclaredMethods()) { if (method.getName().startsWith(SETTER)) { for (MockObjectMap mockObjectMap : objs) { Object obj = method.getGenericParameterTypes()[0]; if (obj instanceof java.lang.reflect.Type && mockObjectMap.getType().getName().equalsIgnoreCase(((Class) obj).getName())) { method.invoke(object, mockObjectMap.getObj()); continue; } } } } claz = claz.getSuperclass(); } while (!claz.equals(Object.class)); } } private void autowireMockBean(Class clz, Object bean, List<MockObjectMap> objs) throws IllegalArgumentException, IllegalAccessException { for (Field field : clz.getFields()) { Annotation[] mockAnnotations = field.getAnnotations(); for (Annotation annotation : mockAnnotations) { if (annotation instanceof org.mockito.Mock) { MockObjectMap mockObjectMap = new MockObjectMap(); objs.add(mockObjectMap); mockObjectMap.setType(field.getType()); mockObjectMap.setObj(mock(field.getType())); field.setAccessible(true); field.set(bean, mockObjectMap.getObj()); continue; } } } } /** * 取得测试类中所有的mock对象的类型 * * @param clazz * @return */ private Class[] getMockClass(Class claz) { List<Class> clasList = new ArrayList<Class>(); Field[] fields = claz.getDeclaredFields(); for (Field field : fields) { Annotation[] mockAnnotations = field.getAnnotations(); for (Annotation annotation : mockAnnotations) { if (annotation instanceof org.mockito.Mock) { clasList.add(field.getType()); continue; } } } return clasList.toArray(new Class[0]); } /** * 取得测试类中测试桩类 * * @param clazz * @return * @throws InvocationTargetException * @throws IllegalAccessException * @throws IllegalArgumentException */ private List<Object> getStubInstance(Class clazz, Object bean) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { List<Object> objList = new ArrayList<Object>(); Field[] fields = clazz.getDeclaredFields();// 测试类中所有的域名 Method[] methods = clazz.getDeclaredMethods(); for (Field field : fields) { Annotation[] mockAnnotations = field.getAnnotations(); for (Annotation annotation : mockAnnotations) { if (annotation instanceof Autowired) { for (Method method : methods) { String name = field.getName(); if (method.getName().startsWith(GETTER) && method.getName().substring(3).equalsIgnoreCase(name)) { objList.add(method.invoke(bean, null)); // 将所有的测试桩类放在objList } } } } } return objList; } private class MockObjectMap { private Object obj; private Class<?> type; public Object getObj() { return obj; } public void setObj(Object obj) { this.obj = obj; } public Class<?> getType() { return type; } public void setType(Class<?> type) { this.type = type; } }}
总结:这种方式可以真正的用spring来实现TDD面向接口的测试方案,对依赖的类做到完全屏蔽,对目前测试类和mock类设置期望输出简单实现