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

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


当前回答

在使用ExpectedException时,对Cem Catikka的答案进行了快速补充:

请记住,预期的异常将被包装在InvocationTargetException中,因此为了获得异常,必须抛出收到的InvocationTargetException的原因。类似于(在BizService上测试私有方法validateRequest()):

@Rule
public ExpectedException thrown = ExpectedException.none();

@Autowired(required = true)
private BizService svc;


@Test
public void testValidateRequest() throws Exception {

    thrown.expect(BizException.class);
    thrown.expectMessage(expectMessage);

    BizRequest request = /* Mock it, read from source - file, etc. */;
    validateRequest(request);
}

private void validateRequest(BizRequest request) throws Exception {
    Method method = svc.getClass().getDeclaredMethod("validateRequest", BizRequest.class);
    method.setAccessible(true);
    try {
        method.invoke(svc, request);
    }
    catch (InvocationTargetException e) {
        throw ((BizException)e.getCause());
    }
 }

其他回答

如果您有一些遗留的Java应用程序,并且不允许更改方法的可见性,那么测试私有方法的最佳方法是使用反射。

在内部,我们使用助手来获取/设置私有和私有静态变量,以及调用私有和私有的静态方法。以下模式将允许您执行与私有方法和字段相关的任何操作。当然,您不能通过反射来更改私有静态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);

笔记:TargetClass.getDeclaredMethod(methodName,argClasses)允许您查看私有方法。同样的事情也适用于getDeclaredField。需要setAccessible(true)才能与private一起玩。

测试私有方法会破坏类的封装,因为每次更改内部实现时都会破坏客户端代码(在本例中是测试)。

所以不要测试私有方法。

JUnit.org常见问题解答页面的答案:

但如果你必须。。。如果您使用的是JDK1.3或更高版本,则可以使用反射来颠覆访问控制机制。有关如何使用它的详细信息,请阅读本文。如果您使用的是JDK1.6或更高版本,并且使用@测试时,可以使用Dp4j在测试方法中注入反射。对于有关如何使用它的详细信息,请参阅此测试脚本。

P.S.我是Dp4j的主要贡献者。问我你是否需要帮助。:)

在过去,我曾为Java使用过反射,在我看来这是一个很大的错误。

严格来说,您不应该编写直接测试私有方法的单元测试。您应该测试的是类与其他对象的公共契约;您不应该直接测试对象的内部。如果另一个开发人员想要对类进行一个小的内部更改,这不会影响类的公共契约,那么他/她就必须修改基于反射的测试,以确保它正常工作。如果在整个项目中重复这样做,那么单元测试就不再是代码健康状况的有用度量,而开始成为开发的障碍,成为开发团队的烦恼。

相反,我建议使用一个代码覆盖工具,例如Cobertura,以确保您编写的单元测试在私有方法中提供代码的适当覆盖。通过这种方式,您可以间接测试私有方法正在做什么,并保持更高级别的灵活性。

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

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

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