测试工具
前段时间的时候,在写很多单元测试,用了比较多的Mockito。
但是有个比较麻烦的事情就是需要调用很多的set方法,甚至有部分被mock的类使用了Spring的注解来注入,并没有使用set方法来赋值,就造成了无法对该属性初始化的尴尬。
于是有了以下的工具:
使用该注解,可以标注在测试类或属性上。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author ruteng.lrt 乱域
* @version $Id: TestClass.java, v 1.2 2016/9/2 16:28 ruteng.lrt Exp $
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.FIELD})
public @interface MockedClass {
String[] value() default {};
}
下面这个是一个自定义的异常类,如果错误使用注解,则会抛出相关的异常。
/**
* @author ruteng.lrt 乱域
* @version $Id: MockedClassException.java, v 1.2 2016/9/2 16:42 ruteng.lrt Exp $
*/
public class MockedClassException extends RuntimeException {
public MockedClassException(MockedEnum mocked){
super(mocked.desc);
}
public static enum MockedEnum{
NEED_CLASS_NAME("注解在类上,需要写类名"),
NEED_MOCKED_ANNO("未找到MockedClass注解");
private String desc;
MockedEnum(String desc){
this.desc = desc;
}
}
}
下面是注解处理器,也是最主要的处理逻辑:
import com.alipay.mobilerelation.common.util.LoggerUtil;
import org.mockito.Mock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
/**
* @author ruteng.lrt 乱域
* @version $Id: MockedClassProcess.java, v 1.2 2016/9/2 17:17 ruteng.lrt Exp $
*/
public class MockedClassProcess {
private static Logger LOGGER = LoggerFactory.getLogger(MockedClassProcess.class);
public static <T> void doProcess(T obj) {
// 获得当前的运行类名
Class<?> clz = obj.getClass();
Set<Field> fieldSet = new HashSet<Field>();
Set<Field> mockedFieldSet = new HashSet<Field>();
Field[] declaredFields = clz.getDeclaredFields();
for (Field f : declaredFields) {
Mock mock = f.getAnnotation(Mock.class);
if (null != mock) {
// 找到所有的需要mock的类
fieldSet.add(f);
} else if (null != f.getAnnotation(MockedClass.class)) {
// 如果该类是被测试的类,则设置
mockedFieldSet.add(f);
}
}
MockedClass mocked = clz.getAnnotation(MockedClass.class);
if (null == mocked ) {
if(mockedFieldSet.size() == 0){
throw new MockedClassException(MockedClassException.MockedEnum.NEED_MOCKED_ANNO);
}
}else {
String[] fieldNames = mocked.value();
if (fieldNames.length == 0) {
throw new MockedClassException(MockedClassException.MockedEnum.NEED_CLASS_NAME);
}
try {
for (String field : fieldNames) {
mockedFieldSet.add(clz.getDeclaredField(field));
}
} catch (NoSuchFieldException e) {
LoggerUtil.error(e);
}
}
initField(obj, mockedFieldSet, fieldSet);
}
/**
* 对field设置属性
* @param obj 调用对象
* @param mockedfield 需要设置的类
* @param fieldSet 被设置的属性
*/
private static <T> void initField(T obj, Set<Field> mockedfield, Set<Field> fieldSet) {
for(Field field : mockedfield) {
field.setAccessible(true);
try {
Object o = field.get(obj);
Class<?> mockedClz = field.getType(); // mock的类
Class<?> tempClz = mockedClz; // 用来恢复原始类
if (null == o) {
o = mockedClz.newInstance();
}
field.set(obj, o);
for (Field f : fieldSet) { // 测试类的field
f.setAccessible(true);
boolean flag = false;
while (!flag) { // 在该类未找到的时候,从父类找
try {
Method setMethod = mockedClz.getDeclaredMethod(getSetMethodName(f.getName()), f.getType());
setMethod.invoke(o, f.get(obj));
flag = true;
} catch (NoSuchMethodException e) {
try {
Field mockedField = mockedClz.getDeclaredField(f.getName());// 待测试类中的field
mockedField.setAccessible(true);
mockedField.set(o, f.get(obj));
flag = true;
} catch (NoSuchFieldException ignored) {
mockedClz = mockedClz.getSuperclass();
if(mockedClz.getName().equals("java.lang.Object")){
flag = true;
}
}
} catch (InvocationTargetException e) {
LoggerUtil.error(e);
}
}
mockedClz = tempClz; // 恢复原始类
}
} catch (IllegalAccessException e) {
LoggerUtil.error(e);
} catch (InstantiationException e) {
LoggerUtil.error(e);
}
}
}
/**
* 通过field的名字获取set方法的名字
* @param str
* @return
*/
private static String getSetMethodName(String str) {
StringBuilder sb = new StringBuilder();
sb.append("set").append(Character.toUpperCase(str.charAt(0))).append(str.substring(1));
return sb.toString();
}
}
测试工具的使用
1.0 版本
User类:POJO
/**
* @author ruteng.lrt 乱域
* @version $Id: User.java, v 0.1 2016/9/2 16:51 ruteng.lrt Exp $
*/
public class User {
private String userName;
private int age;
private OtherObj obj;
/**
* Getter method for property <tt>userName</tt>.
*
* @return property value of userName
*/
public String getUserName() {
return userName;
}
/**
* Getter method for property <tt>age</tt>.
*
* @return property value of age
*/
public int getAge() {
return age;
}
/**
* Getter method for property <tt>obj</tt>.
*
* @return property value of obj
*/
public OtherObj getObj() {
return obj;
}
}
OtherObj类,作为其他对象
/**
* @author ruteng.lrt 乱域
* @version $Id: OtherObj.java, v 0.1 2016/9/2 16:52 ruteng.lrt Exp $
*/
public class OtherObj {
public String sayObj(String say){
System.out.println(say);
return say;
}
}
UserTest类:测试类
做了一个测试工具类,从此Mock数据的时候,在也不需要向被test的类中写入n多个set方法。
使用说明:
1. 在被测试的类上增加注解@MockedClass
或在测试类(UserTest)上增加注解@MockedClass(“user”)
;
- 此field可以是static/final的,也可以是一般Field;
- 此field可以不经过初始化(会内部默认初始化),但需要提供默认无参构造方法;
2. 在init()方法中调用MockedClassProcess.doProcess(this)即可。
3. 必须保证被测试类User与测试类UserTest中的field的命名一致,否则会报错。
4. 测试类可以没有set方法,此特性可以针对使用@OsgiReference注入的时候没有写set方法的尴尬。
版本迭代
1.1 版本:
修改process,优先使用set方法进行赋值,提高代码覆盖率
1.2 版本
1) 修改MockedClass注解,现在可以在一个test类里面同时标记多个@MockedClass注解,用来同时测试的多个类
2) 修复被测试的类如果有属性是继承自父类,无法对属性赋值的bug