unit-testing - test介紹 - unit test教學




如何測試私有函數或具有私有方法,字段或內部類的類? (20)

JUnit.org FAQ頁面的答案:

但如果你必須......

如果您使用的是JDK 1.3或更高版本,則可以使用反射在PrivilegedAccessor的幫助下破壞訪問控制機制。 有關如何使用它的詳細信息,請閱讀本文

如果您使用的是JDK 1.6或更高版本並使用@Test註釋測試,則可以使用Dp4j在測試方法中註入反射。 有關如何使用它的詳細信息,請參閱此測試腳本

PS我是Dp4j的主要貢獻者,問me是否需要幫助。 :)

如何對具有內部私有方法,字段或嵌套類的類進行單元測試(使用xUnit)? 或者通過內部鏈接 (在C / C ++中為static )或在私有( anonymous )命名空間中使其成為私有的函數?

為了能夠運行測試而改變方法或函數的訪問修飾符似乎很糟糕。


只是我想要測試私有方法的兩個例子:

  1. 解密例程 - 我不想讓任何人看到它只是為了測試,否則任何人都可以使用它們來解密。 但它們是代碼固有的,複雜的,並且需要始終工作(明顯的例外是反射,在大多數情況下,當SecurityManager未配置為阻止此操作時,甚至可以用於查看私有方法)。
  2. 創建用於社區消費的SDK 。 這裡公開具有完全不同的含義,因為這是整個世界可能看到的代碼(不僅僅是我的應用程序的內部代碼)。 如果我不希望SDK用戶看到它,我會將代碼放入私有方法中 - 我不認為這是代碼味道,僅僅是SDK編程的工作原理。 但是我當然還需要測試我的私有方法,而且它們實際上是我的SDK的功能所在。

我理解只測試“合同”的想法。 但是我沒有看到人們可以提倡實際上沒有測試代碼 - 你的里程可能會有所不同。

因此,我的權衡涉及使JUnits與反射複雜化,而不是損害我的安全性和SDK。


在嘗試了使用 Java 反射的 Cem Catikkas 解決方案之後 ,我不得不說他的解決方案比我在這裡描述的更優雅。 但是,如果您正在尋找使用反射的替代方法,並且可以訪問您正在測試的源,那麼這仍然是一個選項。

測試類的私有方法可能是有價值的,特別是在測試驅動開發中 ,您希望在編寫任何代碼之前設計小型測試。

創建可訪問私有成員和方法的測試可以測試難以專門定位的代碼區域,只能訪問公共方法。 如果公共方法涉及多個步驟,則它可以包含多個私有方法,然後可以單獨進行測試。

好處:

  • 可以測試更精細的粒度

缺點:

  • 測試代碼必須與源代碼位於同一文件中,這可能更難以維護
  • 與.class輸出文件類似,它們必須保持在源代碼中聲明的相同包中

但是,如果連續測試需要這種方法,則可能是應該提取私有方法的信號,這可以用傳統的公共方式進行測試。

這是一個如何工作的複雜的例子:

// Import statements and package declarations

public class ClassToTest
{
    private int decrement(int toDecrement) {
        toDecrement--;
        return toDecrement;
    }

    // Constructor and the rest of the class

    public static class StaticInnerTest extends TestCase
    {
        public StaticInnerTest(){
            super();
        }

        public void testDecrement(){
            int number = 10;
            ClassToTest toTest= new ClassToTest();
            int decremented = toTest.decrement(number);
            assertEquals(9, decremented);
        }

        public static void main(String[] args) {
            junit.textui.TestRunner.run(StaticInnerTest.class);
        }
    }
}

內部類將編譯為ClassToTest$StaticInnerTest

另請參閱: Java技巧106:用於娛樂和利潤的靜態內部類


如果使用Spring, ReflectionTestUtils提供了一些方便的工具,只需很少的工作就可以幫助你。 例如,要在私有成員上設置模擬而不必強制添加不需要的公共setter:

ReflectionTestUtils.setField(theClass, "theUnsettableField", theMockObject);

如果您正在使用JUnit,請查看junit-addons 。 它能夠忽略Java安全模型並訪問私有方法和屬性。


如果您正在嘗試測試您不願意或無法更改的現有代碼,那麼反射是一個不錯的選擇。

如果類的設計仍然是靈活的,並且你有一個複雜的私有方法,你想單獨測試,我建議你把它拉到一個單獨的類,並分別測試該類。 這不必更改原始類的公共接口; 它可以在內部創建輔助類的實例並調用輔助方法。

如果您想測試來自輔助方法的困難錯誤條件,您可以更進一步。 從helper類中提取一個接口,將一個公共getter和setter添加到原始類中以注入幫助器類(通過其接口使用),然後將一個helper類的模擬版本注入到原始類中以測試原始類的方式響應助手的異常。 如果您想在不測試幫助程序類的情況下測試原始類,這種方法也很有用。



我使用的另一種方法是將私有方法更改為包私有或受保護,然後使用Google Guava庫的@VisibleForTesting註釋對其進行補充。

這將告訴使用此方法的任何人要小心,即使在包中也不能直接訪問它。 另外,測試類不需要在物理上相同的包中,而是在測試文件夾下的相同包中。

例如,如果要測試的方法是在src/main/java/mypackage/MyClass.java那麼您的測試調用應該放在src/test/java/mypackage/MyClassTest.java 。 這樣,您就可以訪問測試類中的測試方法。


我建議你稍微重構一下你的代碼。 當你必須開始考慮使用反射或其他類型的東西時,為了測試你的代碼,你的代碼就會出錯。

你提到了不同類型的問題。 讓我們從私人領域開始。 在私有字段的情況下,我會添加一個新的構造函數並將注入的字段注入其中。 而不是這個:

public class ClassToTest {

    private final String first = "first";
    private final List<String> second = new ArrayList<>();
    ...
}

我用過這個:

public class ClassToTest {

    private final String first;
    private final List<String> second;

    public ClassToTest() {
        this("first", new ArrayList<>());
    }

    public ClassToTest(final String first, final List<String> second) {
        this.first = first;
        this.second = second;
    }
    ...
}

即使使用一些遺留代碼,這也不會成為問題。 舊代碼將使用空構造函數,如果您問我,重構代碼看起來更乾淨,並且您將能夠在沒有反射的情況下在測試中註入必要的值。

現在關於私人方法。 根據我的個人經驗,當您必須存根私有方法進行測試時,該方法與該類無關。 在這種情況下,一個常見的模式是包裝在一個接口中,比如Callable ,然後你也在構造函數中傳入該接口(使用那個多個構造函數):

public ClassToTest() {
    this(...);
}

public ClassToTest(final Callable<T> privateMethodLogic) {
    this.privateMethodLogic = privateMethodLogic;
}

大多數我寫的所有內容看起來都是依賴注入模式。 根據我的個人經驗,它在測試時非常有用,我認為這種代碼更清晰,更易於維護。 我會對嵌套類說同樣的話。 如果嵌套類包含重邏輯,那麼如果將它作為包私有類移動並將其註入需要它的類中會更好。

我在重構和維護遺留代碼時也使用了其他幾種設計模式,但這完全取決於要測試的代碼的情況。使用反射主要不是問題,但是當你有一個經過嚴格測試的企業應用程序並且在每次部署之前運行測試時,一切都變得非常慢(這很煩人,我不喜歡那種東西)。

還有二次注射,但我不建議使用它。我最好堅持使用構造函數並在必要時初始化所有內容,留下注入必要依賴項的可能性。


我過去曾使用reflection來為Java做這個,在我看來這是一個很大的錯誤。

嚴格來說,您應該編寫直接測試私有方法的單元測試。 您應該測試的是該類與其他對象的公共合同; 你永遠不應該直接測試對象的內部。 如果另一個開發人員想要對該類進行少量內部更改(這不會影響類公共合同),則他/她必須修改基於反射的測試以確保其有效。 如果您在整個項目中反复執行此操作,則單元測試將不再是對代碼運行狀況的有用度量,並開始成為開發的障礙,並對開發團隊造成煩擾。

我建議改為使用Cobertura等代碼覆蓋工具,以確保您編寫的單元測試能夠在私有方法中提供良好的代碼覆蓋率。 這樣,您可以間接測試私有方法正在做什麼,並保持更高的敏捷性。


正如其他人所說......不要直接測試私人方法。 以下是一些想法:

  1. 保持所有方法小而集中(易於測試,易於發現錯誤)
  2. 使用代碼覆蓋工具。 我喜歡Cobertura (哦,快樂的一天,看起來像是一個新版本!)

在單元測試上運行代碼覆蓋率。 如果您發現方法未經過完全測試,請添加到測試中以獲得覆蓋率。 瞄準100%的代碼覆蓋率,但意識到你可能不會得到它。


測試私有方法會破壞類的封裝,因為每次更改內部實現時都會破壞客戶端代碼(在本例中為測試)。

所以不要測試私有方法。


為了測試具有大型和古怪類的遺留代碼,能夠測試我現在正在編寫的一個私有(或公共)方法通常非常有用。

我使用junitx.util.PrivateAccessor -package for Java。 許多有用的單行程序用於訪問私有方法和私有字段。

import junitx.util.PrivateAccessor;

PrivateAccessor.setField(myObjectReference, "myCrucialButHardToReachPrivateField", myNewValue);
PrivateAccessor.invoke(myObjectReference, "privateMethodName", java.lang.Class[] parameterTypes, java.lang.Object[] args);

當我在一個足夠複雜的類中有私有方法時,我覺得需要直接測試私有方法,這就是代碼味道:我的類太複雜了。

我解決這些問題的常用方法是梳理一個包含有趣位的新類。 通常,此方法及其與之交互的字段可能會被提取到新類中。

新類將這些方法公開為“公共”,因此它們可以進行單元測試。 新舊課程現在都比原來的課程簡單,這對我來說很棒(我需要保持簡單,否則我會迷路!)。

請注意,我並不是說人們在不使用大腦的情況下創建課程! 這裡的要點是使用單元測試的力量來幫助您找到好的新類。


私有方法由公共方法使用。 否則,它們就是死代碼。 這就是為什麼你測試公共方法,斷言公共方法的預期結果,從而斷言它消耗的私有方法。

在對公共方法運行單元測試之前,應通過調試來測試私有方法的測試。

它們也可以使用測試驅動開發進行調試,調試單元測試直到滿足所有斷言。

我個人認為使用TDD創建類更好; 創建公共方法存根,然後使用預先定義的所有斷言生成單元測試,因此在編碼之前確定方法的預期結果。 這樣,您就不會走錯路徑,使單元測試斷言符合結果。 您的課程非常強大,並且在所有單元測試通過後都符合要求。


私有方法由公共方法調用,因此公共方法的輸入還應測試由這些公共方法調用的私有方法。 當公共方法失敗時,那可能是私有方法失敗。


這是我測試私有字段的通用函數:

protected <F> F getPrivateField(String fieldName, Object obj)
    throws NoSuchFieldException, IllegalAccessException {
    Field field =
        obj.getClass().getDeclaredField(fieldName);

    field.setAccessible(true);
    return (F)field.get(obj);
}

通常,單元測試旨在運用類或單元的公共接口。 因此,私有方法是您不希望顯式測試的實現細節。


對於Java我會使用reflection,因為我不喜歡只是為了測試而在聲明的方法上更改對包的訪問的想法。但是,我通常只測試公共方法,這也應該確保私有方法正常工作。

您不能使用反射從所有者類外部獲取私有方法,私有修飾符也會影響反射

這不是真的。你肯定可以,正如Cem Catikkas的回答中提到的那樣。


我最近遇到了這個問題並寫了一個名為Picklock的小工具,它避免了顯式使用Java反射API的問題,兩個例子:

調用方法,例如private void method(String s)- 通過Java反射

Method method = targetClass.getDeclaredMethod("method", String.class);
method.setAccessible(true);
return method.invoke(targetObject, "mystring");

調用方法,例如private void method(String s)- 由Picklock

interface Accessible {
  void method(String s);
}

...
Accessible a = ObjectAccess.unlock(targetObject).features(Accessible.class);
a.method("mystring");

設置字段,例如private BigInteger amount;- 通過Java反射

Field field = targetClass.getDeclaredField("amount");
field.setAccessible(true);
field.set(object, BigInteger.valueOf(42));

設置字段,例如private BigInteger amount;- 由Picklock

interface Accessible {
  void setAmount(BigInteger amount);
}

...
Accessible a = ObjectAccess.unlock(targetObject).features(Accessible.class);
a.setAmount(BigInteger.valueOf(42));




tdd