2010-05-11 24 views
80

在過去的幾年中,我一直認爲在Java中,Reflection在單元測試中被廣泛使用。由於一些必須檢查的變量/方法是私人的,因此需要讀取它們的值。我一直認爲Reflection API也用於此目的。在單元測試中使用反射是不好的做法嗎?

上週我不得不測試一些軟件包,因此寫了一些JUnit測試。與往常一樣,我使用Reflection來訪問私人字段和方法。但是,我的主管檢查了代碼並不滿意,並告訴我反射API並非用於這種「黑客行爲」。相反,他建議修改生產代碼中的可見性。

使用反射真的是不好的做法嗎?我真的不能相信─

編輯:我應該提到,我是要求所有的測試都在名爲測試一個單獨的包(因此使用受保護的公開程度例如是不是一個可行的解決方案太)

+0

你用反射,設置,獲得或兩者練習什麼樣的訪問? – fish 2010-05-11 14:00:08

+0

@fish:我只使用get來驗證是否設置了一些特定的值 – RoflcoptrException 2010-05-11 14:02:02

+7

Java中的常規做法是將測試放在同一個包中,以便測試「包專用」(默認訪問)類。 – 2010-05-11 14:17:23

回答

58

恕我直言反射應該只是最後的手段,保留爲單元測試遺留代碼的特殊情況或API你不能改變。如果您正在測試自己的代碼,那麼您需要使用Reflection的事實意味着您的設計不可測試,因此您應該修復該問題而不是訴諸Reflection。

如果您需要在單元測試中訪問私有成員,它通常意味着所討論的類具有不合適的接口,並且/或者嘗試做太多。所以要麼修改它的接口,要麼將一些代碼提取到一個單獨的類中,在那裏可以公開那些有問題的方法/字段訪問器。

請注意,使用反射通常會導致代碼除了難以理解和維護外,還更脆弱。在正常情況下,會有一整套錯誤被編譯器檢測到,但使用Reflection時,它們只會出現運行時異常。

更新:作爲@tackline指出,這隻涉及在自己的測試代碼中使用反射,而不是測試框架的內部。 JUnit(也可能是所有其他類似的框架)使用反射來識別和調用您的測試方法 - 這是對反射的合理和本地化使用。無需使用反射就可以提供相同的功能和便利。 OTOH它完全封裝在框架實現中,所以它不會使我們自己的測試代碼複雜化或不妥協。

+9

作爲編譯時註釋處理器的替代方法,反射在調用測試和模擬接口的測試框架內顯然是有用的。但帽子應作爲一種單獨的用法明確指出。 – 2010-05-11 14:19:04

+1

@tackline,這是我真正的意思,謝謝指出。我在答覆中加了一個說明。 – 2010-05-11 14:34:57

+0

你不需要很多限定符。反思應該主要用作最後的手段。它使關於該程序的推理實際上不可能。 – 2011-04-14 06:40:19

56

確實是僅僅爲了測試而修改生產API的可見性。出於正當理由,該可見度很可能會設置爲當前值,並且不會更改。

使用反射進行單元測試基本上沒問題。當然,你應該design your classes for testability,所以反射較少是必需的。彈簧例如有ReflectionTestUtils。但它的目的是設置模擬依賴,春天應該注入它們。

的主題是不是「做&不要」更深,並認爲什麼應該被測試 - 是否需要測試或沒有對象的內部狀態;我們是否應該對被試課程的設計提出質疑;等

+1

你會解釋你的downvote? – Bozho 2010-05-11 13:58:31

+6

生產API改變有兩個方面。只是爲了測試而改變它是不好的,在更新你的API /設計的時候,讓你的代碼更易於測試(因此你不會僅僅爲了測試而改變你的可見性)是一個好習慣。 – deterb 2010-05-11 16:21:48

+1

如果你確定你在做什麼沒有副作用 - 是的。 ;) – Bozho 2010-05-11 16:25:35

7

我認爲這是一個不好的做法,但只是改變生產代碼的可見性不是一個好的解決方案,你必須看看原因。要麼你有一個無法測試的API(也就是說API沒有公開足夠的測試來獲取句柄),所以你想要測試私有狀態,或者你的測試與你的實現相結合,這將使他們當你重構時只有邊際使用。

不知道更多關於你的情況,我真的不能多說,但它確實被認爲是一個不好的做法,使用reflection。就個人而言,我寧願讓測試成爲測試類的靜態內部類而不是求助於反射(如果說API的不可測試部分不受我的控制),但是有些地方會遇到更大的問題,測試代碼處於與使用反射相同的產品代碼包。

編輯:爲了迴應你的編輯,至少與使用反射差一樣的做法,可能更糟。通常處理的方式是使用相同的包,但將測試保存在單獨的目錄結構中。如果單元測試不屬於與被測試的類相同的包,我不知道是什麼。

不管怎樣,你還是可以解決這個問題,通過使用受保護的(可惜不是包專用這實在是非常理想的)通過測試的子類,像這樣:

public class ClassUnderTest { 
     protect void methodExposedForTesting() {} 
} 

而且你的單元測試中

class ClassUnderTestTestable extends ClassUnderTest { 
    @Override public void methodExposedForTesting() { super.methodExposedForTesting() } 
} 

如果你有一個受保護的構造:

ClassUnderTest test = new ClassUnderTest(){}; 

我並不一定建議上述情況適用於正常情況,但您已被要求使用的限制並非「最佳實踐」。

+0

+1用於更改生產代碼可見性不是一個好的解決方案 – 2010-05-11 14:03:15

3

我認爲你的代碼應該通過兩種方式來測試。你應該通過單元測試來測試你的公共方法,這將作爲我們的黑盒測試。因爲你的代碼被分解成可管理的功能(好的設計),所以你需要用反射來單獨測試各個部分,以確保它們獨立於流程工作,我認爲這樣做的唯一方式是使用反射他們是私人的。

至少,這是我在單元測試過程中的想法。

28

從TDD的角度來看 - 測試驅動設計 - 這是一個不好的做法。我知道你沒有標記這個TDD,也沒有專門詢問它,但TDD是一個很好的做法,這與它的穀物背道而馳。在TDD中,我們使用我們的測試來定義類的接口 - public接口 - 因此我們正在編寫只與公共接口交互的測試。我們關心那個接口;適當的訪問級別是設計的重要組成部分,是良好代碼的重要組成部分。如果你發現自己需要測試一些私密的東西,那麼通常是以我的經驗來看,它是一種設計氣味。

+3

同意。將待測物體當作黑匣子 - 確保輸出與輸入相匹配。如果他們不知道你在實現的內部存在問題。 – AngerClown 2010-05-12 17:39:41

+0

但是測試對象狀態呢? – 2015-01-22 11:38:45

+5

只要外部行爲正確,對象的內部狀態就無關緊要。 – 2015-01-22 13:23:54

3

要添加到其他人所說,考慮以下因素:

//in your JUnit code... 
public void testSquare() 
{ 
    Class classToTest = SomeApplicationClass.class; 
    Method privateMethod = classToTest.getMethod("computeSquare", new Class[]{Integer.class}); 
    String result = (String)privateMethod.invoke(new Integer(3)); 
    assertEquals("9", result); 
} 

在這裏,我們使用反射來執行私有方法SomeApplicationClass.computeSquare(),傳遞一個整數並返回一個字符串結果。這導致在一個JUnit測試將編譯很好,但在執行過程中失敗,如果任何的以下發生:

  • 方法名稱「computeSquare」被重命名
  • 該方法以在不同的參數類型(例如,改變的整數,以龍)
  • 參數變化(例如通過在其它參數)
  • 的方法的變化(可能是從字符串到整數)的返回類型的

不是件數,如果有沒有簡單的方法來證明computeSquare已經完成了它應該通過公共API完成的任務,那麼你的類可能會做很多事情。拉這個方法到一個新的類,它爲您提供了以下測試:

public void testSquare() 
{ 
    String result = new NumberUtils().computeSquare(new Integer(3)); 
    assertEquals("9", result); 
} 

現在(特別是當你使用現代IDE的可重構工具),改變方法的名字對你的測試沒有影響(如您的IDE也將重構JUnit測試),同時更改參數類型,方法的數量或返回類型將在您的Junit測試中標記編譯錯誤,這意味着您將不會檢查編譯的JUnit測試但在運行時失敗。

我的最後一點是,有時候,特別是在使用遺留代碼時,並且需要添加新功能時,在單獨編寫好的可測試類中這樣做可能並不容易。在這種情況下,我的建議是將新的代碼更改隔離到受保護的可見性方法,您可以直接在JUnit測試代碼中執行這些方法。這使您可以開始建立測試代碼的基礎。最終,您應該重構該類並提取您的附加功能,但與此同時,新代碼的受保護的可見性有時可以成爲您在測試性方面的最佳方式,而無需進行重大重構。

5

我第二Bozho的想法:

這是非常糟糕的修改生產API的知名度只是用於測試的緣故。

但是,如果您嘗試do the simplest thing that could possibly work那麼它可能會更喜歡編寫Reflection API樣板,至少在您的代碼仍然是新/更改時。我的意思是,每次更改方法名稱或參數時手動更改來自測試的反射調用的負擔是恕我直言,太多的負擔和錯誤的焦點。

已經下降放寬無障礙只是用於測試的陷阱,然後不經意地訪問來自其他生產代碼我想的dp4j.jar的是,私有方法:它注入反射API代碼(Lombok-style),這樣你就不會改變生產代碼並且不要自己編寫Reflection API;在編譯時,dp4j會將單元測試中的直接訪問替換爲等效的反射API。這是一個example of dp4j with JUnit

相關問題