測試工具
前段時間的時候,在寫很多單元測試,用了比較多的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