Mock測試工具類的設計與使用

測試工具

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章