如何使用JUnit测试具有内部私有方法、字段或嵌套类的类?

仅仅为了能够运行测试而更改方法的访问修饰符似乎是不好的。


当前回答

我建议您稍微重构一下代码。当你不得不开始考虑使用反射或其他类型的东西来测试你的代码时,你的代码出了问题。

你提到了不同类型的问题。让我们从私有字段开始。对于私有字段,我会添加一个新的构造函数并将字段注入其中。而不是:

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

我写的大部分内容看起来都是依赖注入模式。根据我的个人经验,它在测试时非常有用,我认为这种代码更干净,更容易维护。我对嵌套类也是这样说的。如果嵌套类包含大量逻辑,最好将其作为包私有类移动,并将其注入到需要它的类中。

在重构和维护遗留代码时,我还使用了其他几种设计模式,但这都取决于要测试的代码的情况。大多数情况下,使用反射并不是问题,但当您有一个经过严格测试的企业应用程序,并且在每次部署之前都要运行测试时,一切都会变得非常缓慢(这很烦人,我不喜欢这种东西)。

还有setter注入,但我不建议使用它。我最好使用构造函数,并在真正需要时初始化所有内容,留下注入必要依赖项的可能性。

其他回答

从本文:使用JUnit和SuiteRunner测试私有方法(Bill Venners)中,您基本上有4个选项:

不要测试私有方法。授予方法包访问权限。使用嵌套测试类。使用反射。

私有方法由公共方法调用,因此公共方法的输入也应测试这些公共方法调用的私有方法。当公共方法失败时,这可能是私有方法的失败。

我倾向于不测试私人方法。这里有疯狂。我个人认为,你应该只测试你公开的接口(包括受保护的和内部的方法)。

对于C++(从C++11开始),将测试类添加为好友非常有效,不会破坏生产封装。

让我们假设我们有一些类Foo和一些真正需要测试的私有函数,还有一些类FooTest应该可以访问Foo的私有成员。然后我们应该写下以下内容:

// prod.h: some production code header

// forward declaration is enough
// we should not include testing headers into production code
class FooTest;

class Foo
{
  // that does not affect Foo's functionality
  // but now we have access to Foo's members from FooTest
  friend FooTest;
public:
  Foo();
private:
  bool veryComplicatedPrivateFuncThatReallyRequiresTesting();
}
// test.cpp: some test
#include <prod.h>

class FooTest
{
public:
  void complicatedFisture() {
    Foo foo;
    ASSERT_TRUE(foo.veryComplicatedPrivateFuncThatReallyRequiresTesting());
  }
}

int main(int /*argc*/, char* argv[])
{
  FooTest test;
  test.complicatedFixture();  // and it really works!
}

我感觉完全一样。。。更改一个方法的访问修饰符,以便能够运行测试,这对我来说是个坏主意。在我们公司,我们也进行了很多讨论,在我看来,测试私有方法的好方法是使用Java反射或其他框架,使方法可测试。对于复杂的私有方法,我多次这样做,这有助于保持测试的小型性、可读性和可维护性。

在我阅读了这里的所有答案之后,我只是不同意那些说“如果你需要测试私有方法,那么会有代码气味”或甚至“不要测试私有方法”的人。。。所以我给你举个小例子:

假设我有一个带有一个公共方法和两个私有方法的类:

public class ConwaysGameOfLife {

    private boolean[][] generationData = new boolean[128][128];

    /**
     * Compute the next generation and return the new state
     * Also saving the new state in generationData
     */
    public boolean[][] computeNextGeneration() {
        boolean[][] tempData = new boolean[128][128];

        for (int yPos=0; yPos<=generationData.length; yPos++) {
            for (int xPos=0; xPos<=generationData[yPos].length; xPos++) {
                int neighbors = countNeighbors(yPos, xPos);
                tempData[yPos][xPos] = determineCellState(neighbors, yPos, xPos);
            }
        }

        generationData = tempData;
        return generationData;
    }

    /**
     * Counting the neighbors for a cell on given position considering all the edge cases
     *
     * @return the amount of found neighbors for a cell
     */
    private int countNeighbors(int yPos, int xPos) {}

    /**
     * Determine the cell state depending on the amount of neighbors of a cell and on a current state of the cell
     *
     * @return the new cell state
     */
    private boolean determineCellState(int neighborsAmount, int yPos, int xPos) {}
}

因此,至少对于“countNeighbors”方法,我需要测试八个边缘情况和一些一般情况(直接位于角落的单元格、直接位于矩阵边缘的单元格和位于中间的单元格)。因此,如果我只是试图通过“computeNextGeneration”方法覆盖所有的情况,并且在重构之后,一些测试是红色的,那么识别错误所在的位置可能需要花费时间。

如果我分别测试“determineCellState”和“countNeighbors”,并且在重构和优化之后,“computeNextGeneration”和“determine CellState”的测试是红色的,那么我很确定错误将出现在“determiseCellState”方法中。

此外,如果您从一开始就为这些方法编写单元测试,这些测试将帮助您开发方法/算法,而无需考虑和包装公共方法中的其他方法调用和案例。你只需要在方法中编写快速的小测试来覆盖你的案例。。。例如,如果名为“countNeighbors_should_return_right_amount_of_noughbors_for_the_right_top_corner_cell()”的测试失败,那么很清楚在哪里查找错误。