如何使用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());
    }
 }

其他回答

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

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

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

正如上面许多人所建议的,一个好的方法是通过公共接口测试它们。

如果您这样做,最好使用代码覆盖工具(如EMMA)来查看您的私有方法是否确实在测试中执行。

如果使用Spring,ReflectionTestUtils提供了一些方便的工具,可以帮助您以最小的工作量完成任务。例如,要在私有成员上设置模拟,而不必强制添加不需要的公共setter:

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

我想分享一条关于测试的规则,这条规则尤其与本主题相关:

我认为你不应该为了更轻松地编写测试。

在其他帖子中有一些建议,说你应该调整原始类以测试私有方法-请先将此警告标记为红色。

如果我们将方法/字段的可访问性更改为包私有或受保护,只是为了让测试可以访问它,那么我们就违背了私有访问指令存在的目的。

当我们想要进行测试驱动的开发时,为什么要有私有字段/方法/类?那么,我们是否应该将所有内容都声明为私有的,甚至是公共的,这样我们就可以不费力地进行测试了-我不这么认为。

从另一个角度来看:测试不应影响生产应用程序的性能和执行。

如果我们仅仅为了更容易的测试而更改生产代码,这可能会在某种程度上影响性能和应用程序的执行。

如果有人开始将私有访问更改为包私有,那么开发人员最终可能会想出其他“巧妙的想法”,向原始类添加更多代码。这会给可读性带来额外的噪音,并会影响应用程序的性能。

随着私有访问更改为限制性较小的访问,我们为开发人员在应用程序的未来开发中滥用新情况提供了可能性。我们不是强迫他/她以正确的方式发展,而是用新的可能性引诱他/她,让他/她有能力在未来做出错误的选择。

当然,这条规则可能会有一些例外,但如果理解清楚,规则是什么,例外是什么?我们需要绝对确定我们知道为什么会引入这种例外。

我只测试公共接口,但众所周知,我会保护特定的私有方法,因此我可以完全模拟它们,或者添加特定于单元测试目的的额外步骤。一般情况下,挂接我可以从单元测试中设置的标志,以使某些方法故意导致异常,从而能够测试故障路径;异常触发代码仅在受保护方法的重写实现中的测试路径中。

不过,我尽量不需要这样做,我总是记录下确切的原因,以避免混淆。