對於軟件開發人員來說單元測試是一項必不可少的工作它既可以驗證程序的有效性又可以在程序出現 BUG 的時候幫助開發人員快速的定位問題所在但是在寫單元測試的過程中開發人員經常要訪問類的一些非公有的成員變量或方法這給測試工作帶來了很大的困 擾本文總結了訪問類的非公有成員變量或方法的四種途徑以方便測試人員在需要訪問類非公有成員變量或方法時進行選擇
盡管有很多經驗豐富的程序員認為不應該提倡訪問類的私有成員變量或方法因為這樣做違反了 Java 語言封裝性的基本規則然而在實際測試中被測試的對象千奇百怪為了有效快速的進行單元測試有時我們不得不違反一些這樣或那樣的規則本文只討論如何 訪問類的非公有成員變量或方法至於是否應該在開發測試中這樣做則留給讀者自己根據實際情況去判斷和選擇
方法一修改訪問權限修飾符
先介紹最簡單也是最直接的方法就是利用 Java 語言自身的特性達到訪問非公有成員的目的說白了就是直接將 private 和 protected 關鍵字改為 public 或者直接刪除我們建議直接刪除因為在 Java 語言定義中缺省訪問修飾符是包可見的這樣做之後我們可以另建一個源碼目錄 test 目錄(多數 IDE 支持這麼做如 Eclipse 和 JBuilder)然後將測試類放到 test 目錄相同包下從而達到訪問待測類的成員變量和方法的目的此時在其它包的代碼依然不能訪問這些變量或方法在一定程度上保障了程序的封裝性
下面的代碼示例展示了這一方法
清單 原始待測類 A 代碼
public class A { private String name = null; private void calculate() { }}
清單 針對單元測試修改後的待測類 A 的代碼
public class A { String name = null; private void calculate() { }}
這種方法雖然看起來簡單粗暴但經驗告訴我們這個方法在測試過程中是非常有效的當然由於改變了源代碼雖然只是包可見也已經破壞了對象的封裝性對於多數對代碼安全性要求嚴格的系統此方法並不可取
方法二利用安全管理器
安全性管理器與反射機制相結合也可以達到我們的目的Java 運行時依靠一種安全性管理器來檢驗調用代碼對某一特定的訪問而言是否有足夠的權限具體來說安全性管理器是 javalangSecurityManager 類或擴展自該類的一個類且它在運行時檢查某些應用程序操作的權限換句話說所有的對象訪問在執行自身邏輯之前都必須委派給安全管理器當訪問受到安全 性管理器的控制應用程序就只能執行那些由相關安全策略特別准許的操作因此安全管理器一旦啟動可以為代碼提供足夠的保護默認情況下安全性管理器是沒 有被設置的除非代碼明確地安裝一個默認的或定制的安全管理器否則運行時的訪問控制檢查並不起作用我們可以通過這一點在運行時避開 Java 的訪問控制檢查達到我們訪問非公有成員變量或方法的目的為能訪問我們需要的非公有成員我們還需要使用 Java 反射技術Java 反射是一種強大的工具它使我們可以在運行時裝配代碼而無需在對象之間進行源代碼鏈接從而使代碼更具靈活性在編譯時Java 編譯程序保證了私有成員的私有特性從而一個類的私有方法和私有成員變量不能被其他類靜態引用然而通過 Java 反射機制使得我們可以在運行時查詢以及訪問變量和方法由於反射是動態的因此編譯時的檢查就不再起作用了
下面的代碼演示了如何利用安全性管理器與反射機制訪問私有變量
清單 利用反射機制訪問類的成員變量
//獲得指定變量的值public static Object getValue(Object instance String fieldName) throws IllegalAccessException NoSuchFieldException { Field field = getField(instancegetClass() fieldName) // 參數值為true禁用訪問控制檢查 fieldsetAccessible(true) return fieldget(instance) } //該方法實現根據變量名獲得該變量的值public static Field getField(Class thisClass String fieldName) throws NoSuchFieldException { if (thisClass == null) { throw new NoSuchFieldException(Error field !) }}
其中 getField(instancegetClass() fieldName) 通過反射機制獲得對象屬性如果存在安全管理器方法首先使用 this 和 MemberDECLARED 作為參數調用安全管理器的 checkMemberAccess 方法這裡的 this 是 this 類或者成員被確定的父類 如果該類在包中那麼方法還使用包名作為參數調用安全管理器的 checkPackageAccess 方法 每一次調用都可能導致 SecurityException當訪問被拒絕時這兩種調用方式都會產生 securityexception 異常
setAccessible(true) 方法通過指定參數值為 true 來禁用訪問控制檢查從而使得該變量可以被其他類調用我們可以在我們所寫的類中擴展一個普通的基本類 javalangreflectAccessibleObject 類這個類定義了一種 setAccessible 方法使我們能夠啟動或關閉對這些類中其中一個類的實例的接入檢測這種方法的問題在於如果使用了安全性管理器它將檢測正在關閉接入檢測的代碼是否允許 這樣做如果未經允許安全性管理器拋出一個例外
除訪問私有變量我們也可以通過這個方法訪問私有方法
清單 利用反射機制訪問類的成員方法
public static Method getMethod(Object instance String methodName Class[] classTypes)
throws NoSuchMethodException { Method accessMethod = getMethod(instancegetClass() methodName classTypes)
//參數值為true禁用訪問控制檢查 accessMethodsetAccessible(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 thisClassgetDeclaredMethod(methodName classTypes)
}
catch
(NoSuchMethodException e)
{
return getMethod(thisClassgetSuperclass() methodName classTypes)
}
}
獲得私有方法的原理與獲得私有變量的方法相同
當我們得到了函數後
需要對它進行調用
這時我們需要通過 invoke() 方法來執行對該函數的調用
代碼示例如下
//調用含單個參數的方法public static Object invokeMethod(Object instance
String methodName
Object arg)
throws NoSuchMethodException
IllegalAccessException
InvocationTargetException { Object[] args = new Object[
];
args[
] = arg;
return invokeMethod(instance
methodName
args)
} //調用含多個參數的方法public static Object invokeMethod(Object instance
String methodName
Object[] args)
throws NoSuchMethodException
IllegalAccessException
InvocationTargetException { Class[] classTypes = null;
if (args != null)
{ classTypes = new Class[args
length];
for (int i =
;
i < args
length;
i++)
{ if (args[i] != null)
{ classTypes[i] = args[i]
getClass()
}
}
} return getMethod(instance
methodName
classTypes)
invoke(instance
args)
}
利用安全管理器及反射
可以在不修改源碼 的基礎上訪問私有成員
為測試帶來了極大的方便
尤其是在編譯期間
該方法可以順利地通過編譯
但同時該方法也有一些缺點
第一個是性能問題
用於字段和 方法接入時反射要遠慢於直接代碼
第二個是權限問題
有些涉及 Java 安全的程序代碼並沒有修改安全管理器的權限
此時本方法失效
方法三
使用模仿(Mock)對象
在單元測試的過程中模仿對象被廣泛使用
它從測試中分離了外部的不需要的因素
並且幫助開發人員專注於被測試的功能
模仿對象(Mock object)的核心是構造一個偽類
在測試中通常用這個構造的偽類替換原來的需要訪問相關環境(如應用服務器
數據庫等)的需要測試的待測類
這樣單元 測試便可以運行在本地環境下(這也是對單元測試的基本要求之一
不依賴於任何特定的環境)
並可以正確的執行
此外
由於 Java 語言不能多繼承的特性
使得該方法也可以被用來作為非公有成員變量及方法的訪問方法(測試類不能同時繼承 TestCase 和待測類)
利用該方法
在模仿對象中改變類成員的訪問控制權限
從而達到訪問非公有類變量及方法的目的
下面的代碼示例演示了模仿對象方法
本方法的應用場景在單元測試中非常常見
即在待測試的公有方法中
有一些受限制的成員變量是由其它私有方法來初始化的
在測試該方法的時候
需要給這個變量置初值才能完成測試
清單
待測類 A
public class A { protected String s = null;
public A()
{ } private void method()
{ s =
word
; System
out
println(
this is mock test
)
}
public void makeWord()
{ String prefix = s;
System
out
println(
prefix is:
+ prefix)
}
}
在待測類 A 中
增加工廠方法
清單
包含工廠方法的待測類 A
// 增加工廠方法的類 Apublic class A { protected String s = null;
public A getA() { return new A()
} private void method() { s =
word
; System
out
println(
this is mock test
)
} public void makeWord() { String prefix = s;
System
out
println(
prefix is:
+ prefix)
}}//偽類
在運行時替換類 Apublic class MockA extends A{ public String s = null; public MockA()
{ }}//測試類public class TestA extends TestCase{ public void setup()
{ } public void teardown(){ } public void makeWordTest(){ A a = new MockA()
a
s =
test
; a
makeWord()
}}
此方法中有幾個值得注意的地方
首先是將 創建代碼抽取到工廠方法中
在測試子類中覆蓋該工廠方法
然後令被覆蓋的方法返回模仿對象
如果可以的話
添加需要原始對象的工廠方法的單元測試
以返回 正確類型的對象
模仿對象方法在處理許多對象依賴基礎結構的其它對象或層時
可以起到很好的效果
模仿對象符合實際對象的接口
但只要有足夠的代碼來
欺騙
測試對象並跟蹤其行為
例如
在單元測試中需要測試一個使用數據庫的對象
或者需要測試連接 J
EE 應用服務器的對象
通常的測試用例需要安裝
配置和發送本地數據庫副本
運行測試然後再卸裝本地數據庫或者需要安裝
配置應用服務器
運行測試然後再卸裝 應用服務器
操作可能很麻煩
模仿對象提供了解決這一困難的途徑
對於既需要訪問相關環境又要訪問非公有變量或方法的類來說
模仿對象非常適合
但是
如果只是訪問非公有變量或方法
那麼傳統的模仿對象法顯得有些笨重
可以對該法進行簡化
不使用工廠方法
達到同樣的效果
下面的代碼示例演示了經過簡化的模仿對象方法
清單
簡化的待測類 A 的模仿對象
//偽類
在運行時替換類Apublic class MockA extends A{ public MockA(){ super()
s =
test
;
}
}
//測試類public class TestA extends TestCase{ public void setup(){ } public void teardown(){ } public void makeWordTest(){ A a = new MockA()
a
makeWord()
}
}
模仿對象方法既能消除運行環境的影響
又能解決多繼承的難題
但是由於該方法使用子類的實例來替代父類的實例
對於私有成員變量及方法來說
仍然不能進行訪問
方法四
利用字節碼技術
Java 編譯器把 Java 源代碼編譯成字節碼 bytecode(字節碼)
既然在測試中盡量要避免改變原來的代碼
那麼最直接的改造 Java 類的方法莫過於直接改寫 class 文件
通過修改字節碼中的關鍵字
將私有的成員變量及方法改成公有的成員變量及方法
可以做到在不改變源碼的情況下訪問到需要的成員變量及方法
Java 規范有 class 文件的格式的詳細說明
直接編輯字節碼確實可以改變 Java 類的行為
但是這也要求使用者對 Java class 文件有較深的理解
目前
比較流行的字節碼處理工具有 Javassist
BCEL 和 ASM 等
這幾種工具各有特點
適合於不同的應用場景
如果讀者對字節碼技術感興趣
可以閱讀後面的參考文獻
本文選擇利用字節碼工具 ASM
ASM 能被用來動態生成類或者修改既有類的功能
它可以直接產生二進制 class 文件
也可以在類被加載入 Java 虛擬機之前動態改變類行為
Java class 被存儲在嚴格格式定義的
class 文件裡
這些類文件擁有足夠的元數據來解析類中的所有元素
類名稱
方法
屬性以及 Java 字節碼(指令)
ASM 從類文件中讀入信息後
能夠改變類行為
分析類信息
甚至能夠根據用戶要求生成新類(
class)
ASM 作為 Java 字節碼操控框架
是所有同類工具中效率最高的一個
並且由於其采用了基於 Vistor 模式的框架設計
它也是同類工具中最輕巧靈活的
盡管它的學習台階相對要高一些
它仍然是達到本文目的的首選
利用 ASM 訪問私有變量及方法
需要了解的比較重要的幾個類
ClassReader
ClassVistor
MethodVisitor
FieldVisitor 和 ClassAdaptor 等
ClassReader 類可以直接由字節數組或由 class 文件間接的獲得字節碼數據
它能正確的分析字節碼
通過調用 accept 方法接受一個 ClassVisitor 接口的實現類實例作為參數
然後依次調用 ClassVisitor 接口的各個方法
ClassVisitor 接口中定義了對應 Java 類各個成員的訪問函數
比如 visitMethod 會返回一個實現 MethordVisitor 接口的實例
visitField 會返回一個實現 FieldVisitor 接口的實例
不同 Visitor 的組合
可以非常簡單的封裝對字節碼的各種修改
ClassAdaptor 類為 ClassVisitor 接口提供了一個默認實現
創建一個 ClassAdaptor 對象實例時
需要傳入一個 ClassVisitor 接口的實現類實例來訪問字節嗎
因此當我們需要對字節碼進行調整時
只需從 ClassAdaptor 類派生出一個子類
覆寫需要修改的方法
完成相應功能後再把調用傳遞到下一個需要修改的 visitor 即可
本例的應用場景為
要對公有方法 method() 進行單元測試
但是
該方法中有一個私有變量 number 是由另一個私有方法 makePaper() 付值
所以
需要在測試中為該私有變量置初值
清單
待測類 A
class A{ private String number =
;
public void method()
{ if(number
eaquals(
prefix
))
System
out
println(
method…
+number)
else System
out
println(number +
is null
)
} private void makePaper()
{ number=
prefix
;
System
out
println(
makePaper…
)
}
}
清單
使用字節碼訪問類 A
//修改變量的修飾符public class AccessClassAdapter extends ClassAdapter { public AccessClassAdapter(ClassVisitor cv) { super(cv)
} public FieldVisitor visitField(final int access
String name
final String desc
final String signature
final Object value)
{ int privateAccess = access;
//找到名字為number的變量 if (name
equals(
number
)) privateAccess = Opcodes
ACC_PUBLIC;
//修字段的修飾符為public:在職責鏈傳遞過程中替換調用參數 return cv
visitField(privateAccess
name
desc
signature
value)
} public static void main(String[] args) throws Exception { ClassReader cr = new ClassReader(
A
)
ClassWriter cw = new ClassWriter(ClassWriter
COMPUTE_MAXS)
ClassAdapter classAdapter = new AccessClassAdapter(cw)
cr
accept(classAdapter
ClassReader
SKIP_DEBUG)
byte[] data = cw
toByteArray()
//生成新的字節碼文件 File file = new File(
A
class
)
FileOutputStream fout = new FileOutputStream(file)
fout
write(data)
fout
close()
}
}
執行完該類
將產生一個新的 A
class 文件
測試類測試 method 方法
先對變量進行置初值
然後就可以像其他單元測試一樣
對 method 方法進行測試
回頁首
方法對比
方法 修飾符 使用難度 缺陷
protected 缺省 private
方法一
修改訪問權限修飾符 是 是 是 低
有java編程基礎即可
由於需要修改源代碼
雖然是同包可見
也會帶來一些封閉性的問題
方法二
利用安全性管理器 是 是 是 中
需要了解java安全性管理器及反射機制
一些對代碼安全有要求的程序
程序員並沒有修改security manager的權限
此時
安全管理器方法失效
方法三
使用模仿對象 是 是 否 較高
需要了解設計模式和待測對象的內部實現細節
由於模仿對象要求偽類必需和待測類是繼承與被繼承的關系
所以當源碼以private關鍵字修飾時
此方法失效
方法四
利用字節碼技術 是 是 是 高
需要操作和改寫類部分的字節碼
學習成本高
需要了解Java字節碼技術
總結
在進行單元測試時
我們要盡可能的考慮代碼的移植性和通用性
在不修改源程序的前提下達到測試的最佳效果
對於是否應該使用以及如何使用本文中提到的四種方法
需要開發人員根據具體場合謹慎選擇
From:http://tw.wingwit.com/Article/program/Java/hx/201311/25546.html