熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java核心技術 >> 正文

測試 Java 類的非公有成員變量和方法

2022-06-13   來源: Java核心技術 

  對於軟件開發人員來說單元測試是一項必不可少的工作它既可以驗證程序的有效性又可以在程序出現 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[argslength];
   
    for (int i = ;
   
    i < argslength;
   
    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; Systemoutprintln(this is mock test
   
    }
   
    public void makeWord()
   
    { String prefix = s;
   
    Systemoutprintln(prefix is: + prefix)
   
    }
   
    }
   
    在待測類 A 中增加工廠方法
   
    清單 包含工廠方法的待測類 A
   
    // 增加工廠方法的類 Apublic class A { protected String s = null;
   
    public A getA() { return new A()
   
    } private void method() { s = word; Systemoutprintln(this is mock test
   
    } public void makeWord() { String prefix = s;
   
    Systemoutprintln(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() as = test; amakeWord()
   
    }}
   
    此方法中有幾個值得注意的地方首先是將 創建代碼抽取到工廠方法中在測試子類中覆蓋該工廠方法然後令被覆蓋的方法返回模仿對象如果可以的話添加需要原始對象的工廠方法的單元測試以返回 正確類型的對象模仿對象方法在處理許多對象依賴基礎結構的其它對象或層時 可以起到很好的效果模仿對象符合實際對象的接口但只要有足夠的代碼來欺騙測試對象並跟蹤其行為例如 在單元測試中需要測試一個使用數據庫的對象或者需要測試連接 JEE 應用服務器的對象通常的測試用例需要安裝配置和發送本地數據庫副本運行測試然後再卸裝本地數據庫或者需要安裝配置應用服務器運行測試然後再卸裝 應用服務器操作可能很麻煩模仿對象提供了解決這一困難的途徑對於既需要訪問相關環境又要訪問非公有變量或方法的類來說模仿對象非常適合但是 如果只是訪問非公有變量或方法那麼傳統的模仿對象法顯得有些笨重可以對該法進行簡化不使用工廠方法達到同樣的效果
   
    下面的代碼示例演示了經過簡化的模仿對象方法
   
    清單 簡化的待測類 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()
   
    amakeWord()
   
    }
   
    }
   
    模仿對象方法既能消除運行環境的影響又能解決多繼承的難題但是由於該方法使用子類的實例來替代父類的實例對於私有成員變量及方法來說仍然不能進行訪問


   
    方法四利用字節碼技術
   
    Java 編譯器把 Java 源代碼編譯成字節碼 bytecode(字節碼)既然在測試中盡量要避免改變原來的代碼那麼最直接的改造 Java 類的方法莫過於直接改寫 class 文件通過修改字節碼中的關鍵字將私有的成員變量及方法改成公有的成員變量及方法可以做到在不改變源碼的情況下訪問到需要的成員變量及方法Java 規范有 class 文件的格式的詳細說明直接編輯字節碼確實可以改變 Java 類的行為但是這也要求使用者對 Java class 文件有較深的理解目前比較流行的字節碼處理工具有 JavassistBCEL 和 ASM 等這幾種工具各有特點適合於不同的應用場景如果讀者對字節碼技術感興趣可以閱讀後面的參考文獻本文選擇利用字節碼工具 ASM
   
    ASM 能被用來動態生成類或者修改既有類的功能它可以直接產生二進制 class 文件也可以在類被加載入 Java 虛擬機之前動態改變類行為Java class 被存儲在嚴格格式定義的 class 文件裡這些類文件擁有足夠的元數據來解析類中的所有元素類名稱方法屬性以及 Java 字節碼(指令)ASM 從類文件中讀入信息後能夠改變類行為分析類信息甚至能夠根據用戶要求生成新類(class)ASM 作為 Java 字節碼操控框架是所有同類工具中效率最高的一個並且由於其采用了基於 Vistor 模式的框架設計它也是同類工具中最輕巧靈活的盡管它的學習台階相對要高一些它仍然是達到本文目的的首選
   
    利用 ASM 訪問私有變量及方法需要了解的比較重要的幾個類ClassReaderClassVistorMethodVisitor 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(numbereaquals(prefix))
   
    Systemoutprintln(method…+number)
   
    else Systemoutprintln(number +is null
   
    } private void makePaper()
   
    { number=prefix;
   
    Systemoutprintln(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 (nameequals(number)) privateAccess = OpcodesACC_PUBLIC;
   
    //修字段的修飾符為public:在職責鏈傳遞過程中替換調用參數 return cvvisitField(privateAccess name desc signature value)
   
    } public static void main(String[] args) throws Exception { ClassReader cr = new ClassReader(A
   
    ClassWriter cw = new ClassWriter(ClassWriterCOMPUTE_MAXS)
   
    ClassAdapter classAdapter = new AccessClassAdapter(cw)
   
    craccept(classAdapter ClassReaderSKIP_DEBUG)
   
    byte[] data = cwtoByteArray()
   
    //生成新的字節碼文件 File file = new File(Aclass
   
    FileOutputStream fout = new FileOutputStream(file)
   
    foutwrite(data) foutclose()
   
    }
   
    }
   
    執行完該類將產生一個新的 Aclass 文件
   
    測試類測試 method 方法先對變量進行置初值然後就可以像其他單元測試一樣對 method 方法進行測試
   
    回頁首
   
    方法對比
   
    方法 修飾符 使用難度 缺陷
   
    protected 缺省 private
   
    方法一修改訪問權限修飾符 是 是 是 低有java編程基礎即可 由於需要修改源代碼雖然是同包可見也會帶來一些封閉性的問題
   
    方法二利用安全性管理器 是 是 是 中需要了解java安全性管理器及反射機制 一些對代碼安全有要求的程序程序員並沒有修改security manager的權限此時安全管理器方法失效
   
    方法三使用模仿對象 是 是 否 較高需要了解設計模式和待測對象的內部實現細節 由於模仿對象要求偽類必需和待測類是繼承與被繼承的關系所以當源碼以private關鍵字修飾時此方法失效
   
    方法四利用字節碼技術 是 是 是 高需要操作和改寫類部分的字節碼 學習成本高需要了解Java字節碼技術
   
    總結
   
    在進行單元測試時我們要盡可能的考慮代碼的移植性和通用性在不修改源程序的前提下達到測試的最佳效果對於是否應該使用以及如何使用本文中提到的四種方法需要開發人員根據具體場合謹慎選擇


From:http://tw.wingwit.com/Article/program/Java/hx/201311/25546.html
  • 上一篇文章:

  • 下一篇文章:
  • 推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.