unit-testing test介紹 - 如何測試私有函數或具有私有方法,字段或內部類的類?




tdd是什麼 test教學 (25)

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

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


Answers

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

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

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



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

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

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;
}

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

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

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


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

  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);

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


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


測試私有方法的最佳方法是通過另一種公共方法。 如果無法執行此操作,則滿足下列條件之一:

  1. 私有方法是死代碼
  2. 您正在測試的課程附近有一種設計氣味
  3. 您嘗試測試的方法不應該是私有的

如果您有一些遺留Java應用程序,並且不允許更改方法的可見性,那麼測試私有方法的最佳方法是使用reflection

在內部,我們使用幫助程序來獲取/設置privateprivate static變量,以及調用privateprivate static方法。 以下模式將允許您執行與私有方法和字段相關的任何操作。 當然,您無法通過反射更改private static final變量。

Method method = targetClass.getDeclaredMethod(methodName, argClasses);
method.setAccessible(true);
return method.invoke(targetObject, argObjects);

對於領域:

Field field = targetClass.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);

筆記:
1. targetClass.getDeclaredMethod(methodName, argClasses)讓您查看private方法。 同樣的事情適用於getDeclaredField
2. setAccessible(true)需要使用privates。


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

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

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

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

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


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

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

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

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


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

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);
}

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

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

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


在嘗試了使用 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:用於娛樂和利潤的靜態內部類


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

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

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

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


私有方法只能在同一個類中訪問。 因此,無法從任何測試類測試目標類的“私有”方法。 一種解決方法是您可以手動執行單元測試,也可以將方法從“私有”更改為“受保護”。

然後,只能在定義類的同一個包中訪問受保護的方法。 因此,測試目標類的受保護方法意味著我們需要在與目標類​​相同的包中定義測試類。

如果以上所有內容都不符合您的要求,請使用反射方式訪問私有方法。


請看下面的例子;

應添加以下import語句:

Whitebox.invokeMethod(obj, "privateMethod", "param1");

現在,您可以直接傳遞具有私有方法,要調用的方法名稱以及其他參數的對象,如下所示。

@UiThreadTest
public void testCompute() {

    // Given
    boundBoxOfMainActivity = new BoundBoxOfMainActivity(getActivity());

    // When
    boundBoxOfMainActivity.boundBox_getButtonMain().performClick();

    // Then
    assertEquals("42", boundBoxOfMainActivity.boundBox_getTextViewMain().getText());
}

今天,我推出了一個Java庫來幫助測試私有方法和字段。它在設計時考慮到了Android,但它確實可以用於任何Java項目。

如果您獲得了一些私有方法或字段或構造函數的代碼,則可以使用BoundBox。它完全符合您的要求。下面是一個測試示例,該測試訪問Android活動的兩個私有字段以對其進行測試:

import org.powermock.reflect.Whitebox;

BoundBox可以輕鬆測試私有/受保護的字段,方法和構造函數。您甚至可以訪問通過繼承隱藏的內容。實際上,BoundBox打破了封裝。它將允許您通過反射訪問所有這些,但是在編譯時檢查所有內容。

它是測試一些遺留代碼的理想選擇。仔細使用它。 ;)

BoundBox


正如上面提到的那樣,一個好方法是通過公共接口測試它們。

如果你這樣做,最好使用代碼覆蓋工具(如Emma)來查看你的私有方法是否實際上是從測試中執行的。


我最近遇到了這個問題並寫了一個名為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));

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


我要在這裡把我的帽子扔進戒指裡:

我已經使用了__call黑客成功的混合程度。 我想出的另一種方法是使用訪問者模式:

1:生成一個stdClass或自定義類(強制類型)

2:用所需的方法和參數進行初始化

3:確保你的SUT有一個acceptVisitor方法,它將使用訪問類中指定的參數執行該方法

4:將其註入您想要測試的課程中

5:SUT將操作結果注入訪問者

6:將測試條件應用於訪問者的結果屬性





unit-testing tdd