對於軟件開發人員來說,單元測試是一項必不可少的工作。它既可以驗證程序的有效性,又可以在程序出現 BUG 的時候,幫助開發人員快速的定位問題所在。但是,在寫單元測試的過程中,開發人員經常要訪問類的一些非公有的成員變量或方法,這給測試工作帶來瞭很大的困擾。本文總結瞭訪問類的非公有成員變量或方法的四種途徑,以方便測試人員在需要訪問類非公有成員變量或方法時進行選擇。
盡管有很多經驗豐富的程序員認為不應該提倡訪問類的私有成員變量或方法,因為這樣做違反瞭 Java 語言封裝性的基本規則。然而,在實際測試中被測試的對象千奇百怪,為瞭有效快速的進行單元測試,有時我們不得不違反一些這樣或那樣的規則。本文隻討論如何訪問類的非公有成員變量或方法,至於是否應該在開發測試中這樣做,則留給讀者自己根據實際情況去判斷和選擇。
方法一:修改訪問權限修飾符
先介紹最簡單也是最直接的方法,就是利用 Java 語言自身的特性,達到訪問非公有成員的目的。說白瞭就是直接將 private 和 protected 關鍵字改為 public 或者直接刪除。我們建議直接刪除,因為在 Java 語言定義中,缺省訪問修飾符是包可見的。這樣做之後,我們可以另建一個源碼目錄 —— test 目錄(多數 IDE 支持這麼做,如 Eclipse 和 JBuilder),然後將測試類放到 test 目錄相同包下,從而達到訪問待測類的成員變量和方法的目的。此時,在其它包的代碼依然不能訪問這些變量或方法,在一定程度上保障瞭程序的封裝性。
下面的代碼示例展示瞭這一方法。
清單 1. 原始待測類 A 代碼
public class A { private String name = null; private void calculate() { }}
清單 2. 針對單元測試修改後的待測類 A 的代碼
public class A { String name = null; private void calculate() { }}
這種方法雖然看起來簡單粗暴,但經驗告訴我們這個方法在測試過程中是非常有效的。當然,由於改變瞭源代碼,雖然隻是包可見,也已經破壞瞭對象的封裝性,對於多數對代碼安全性要求嚴格的系統此方法並不可取。
方法二:利用安全管理器
安全性管理器與反射機制相結合,也可以達到我們的目的。Java 運行時依靠一種安全性管理器來檢驗調用代碼對某一特定的訪問而言是否有足夠的權限。具體來說,安全性管理器是 java.lang.SecurityManager 類或擴展自該類的一個類,且它在運行時檢查某些應用程序操作的權限。換句話說,所有的對象訪問在執行自身邏輯之前都必須委派給安全管理器,當訪問受到安全性管理器的控制,應用程序就隻能執行那些由相關安全策略特別準許的操作。因此安全管理器一旦啟動可以為代碼提供足夠的保護。默認情況下,安全性管理器是沒有被設置的,除非代碼明確地安裝一個默認的或定制的安全管理器,否則運行時的訪問控制檢查並不起作用。我們可以通過這一點在運行時避開 Java 的訪問控制檢查,達到我們訪問非公有成員變量或方法的目的。為能訪問我們需要的非公有成員,我們還需要使用 Java 反射技術。Java 反射是一種強大的工具,它使我們可以在運行時裝配代碼,而無需在對象之間進行源代碼鏈接,從而使代碼更具靈活性。在編譯時,Java 編譯程序保證瞭私有成員的私有特性,從而一個類的私有方法和私有成員變量不能被其他類靜態引用。然而,通過 Java 反射機制使得我們可以在運行時查詢以及訪問變量和方法。由於反射是動態的,因此編譯時的檢查就不再起作用瞭。
下面的代碼演示瞭如何利用安全性管理器與反射機制訪問私有變量。
清單 3. 利用反射機制訪問類的成員變量
●Field[] getDeclaredFields():返回已加載類聲明的所有成員變量的Field對象數組,不包括從父類繼承的成員變量.
●Field getDeclaredField(String name):返回已加載類聲明的所有成員變量的Field對象,不包括從父類繼承的成員變量,參數name指定成員變量的名稱.
●Field[] getFields():返回已加載類聲明的所有public型的成員變量的Field對象數組,包括從父類繼承的成員變量
●Field getField(String name):返回已加載類聲明的所有成員變量的Field對象,包括從父類繼承的成員變量,參數name指定成員變量的名稱.
//獲得指定變量的值
public static Object getValue(Object instance, String fieldName)
throws IllegalAccessException, NoSuchFieldException …{
Field field = getField(instance.getClass(), fieldName);
// 參數值為true,禁用訪問控制檢查
field.setAccessible(true);
return field.get(instance);
}
//該方法實現根據變量名獲得該變量的值
public static Field getField(Class thisClass, String fieldName)
throws NoSuchFieldException …{
if (thisClass == null) …{
throw new NoSuchFieldException(“Error field !”);
}
}
其中 getField(instance.getClass(), fieldName) 通過反射機制獲得對象屬性,使用set方法可以重新設置變量的值,如field.set(instance, newValue); 。如果存在安全管理器,方法首先使用 this 和 Member.DECLARED 作為參數調用安全管理器的 checkMemberAccess 方法,這裡的 this 是 this 類或者成員被確定的父類。 如果該類在包中,那麼方法還使用包名作為參數調用安全管理器的 checkPackageAccess 方法。 每一次調用都可能導致 SecurityException。當訪問被拒絕時,這兩種調用方式都會產生 securityexception 異常 。
setAccessible(true) 方法通過指定參數值為 true 來禁用訪問控制檢查,從而使得該變量可以被其他類調用。我們可以在我們所寫的類中,擴展一個普通的基本類 java.lang.reflect.AccessibleObject 類。這個類定義瞭一種 setAccessible 方法,使我們能夠啟動或關閉對這些類中其中一個類的實例的接入檢測。這種方法的問題在於如果使用瞭安全性管理器,它將檢測正在關閉接入檢測的代碼是否允許這樣做。如果未經允許,安全性管理器拋出一個例外。
除訪問私有變量,我們也可以通過這個方法訪問私有方法。
清單 4. 利用反射機制訪問類的成員方法
●Method[] getDeclaredMethods():返回已加載類聲明的所有方法的Method對象數組,不包括從父類繼承的方法.
●Method getDeclaredMethod(String name,Class[] paramTypes):返回已加載類聲明的所有方法的Method對象,不包括從父類繼承的方法,參數name指定方法的名稱,參數paramTypes指定方法的參數類型.
●Method[] getMethods():返回已加載類聲明的所有方法的Method對象數組,包括從父類繼承的方法.
●Method getMethod(String name,Class[] paramTypes):返回已加載類聲明的所有方法的Method對象,包括從父類繼承的方法,參數name指定方法的名稱,參數paramTypes指定方法的參數類型.
public static Method getMethod(Object instance, String methodName, Class[] classTypes)
throws NoSuchMethodException …{
Method accessMethod = getMethod(instance.getClass(), methodName, classTypes);
//參數值為true,禁用訪問控制檢查
accessMethod.setAccessible(true);
return accessMethod;
}
private static Method getMethod(Class thisClass, String methodName, Class[] classTypes)
throws NoSuchMethodException …{
if (thisClass == null) …{
throw new NoSuchMethodException(“Error method !”);
} try …{
return thisClass.getDeclaredMethod(methodName, classTypes);
} catch (NoSuchMethodException e) …{
return getMethod(thisClass.getSuperclass(), methodName, classTypes);
}
}
獲得私有方法的原理與獲得私有變量的方法相同。當我們得到瞭函數後,需要對它進行調用,這時我們需要通過 invoke() 方法來執行對該函數的調用,代碼示例如下:
//調用含單個參數的方法
public static Object invokeMethod(Object instance, String methodName, Object arg)
throws NoSuchMethodException,
IllegalAccessException, InvocationTargetException …{
Object[] args = new Object[1];
args[0] = arg;
return invokeMethod(instance, methodName, args);
}
//調用含多個參數的方法
public static Object invokeMethod(Object instance, String methodName, Object[] args)
throws NoSuchMethodException,
IllegalAccessException, InvocationTargetException …{
Class[] classTypes = null;
&n