在单元测试中使用反射是不好的做法吗?

在过去的几年中,我一直认为在 Java 中,反射在单元测试中得到了广泛的应用。由于必须检查的一些变量/方法是私有的,因此有必要读取它们的值。我一直认为反射 API 也用于此目的。

上周我测试了一些包,因此编写了一些 JUnit 测试。像往常一样,我使用反射来访问私有字段和方法。但是我的上司检查了代码,对此并不满意,他告诉我反射 API 并不是用来进行这种“黑客行为”的。相反,他建议修改生产代码中的可见性。

使用反射真的是不好的做法吗? 我真不敢相信-

Edit: I should have mentioned that i was required that all tests are in a separate package called test (so using protected visibilty e.g. wasn't a possible solution too)

51390 次浏览

IMHO Reflection should really only be a last resort, reserved for the special case of unit testing legacy code or an API you can't change. If you are testing your own code, the fact that you need to use Reflection means your design is not testable, so you should fix that instead of resorting to Reflection.

如果您需要访问单元测试中的私有成员,这通常意味着所讨论的类有一个不合适的接口,并且/或者尝试做太多的事情。因此,要么修改它的接口,要么将一些代码提取到一个单独的类中,在这个类中,那些有问题的方法/字段访问器可以公开。

请注意,在一般情况下使用反射会导致代码更加脆弱,除了难以理解和维护之外。在正常情况下,编译器会检测到一整套错误,但是反射只会作为运行时异常出现。

更新: 正如@tacline 所指出的,这只涉及在自己的测试代码中使用反射,而不涉及测试框架的内部。JUnit (可能还有其他所有类似的框架)使用反射来识别和调用测试方法——这是对反射的合理和本地化使用。如果不使用反射,就很难或不可能提供相同的特性和便利性。OTOH 完全封装在框架实现中,因此它不会使我们自己的测试代码复杂化或受到影响。

仅仅为了测试而修改生产 API 的可见性是不好的。由于合理的原因,该可见性可能被设置为其当前值,并且不会更改。

使用反射进行单元测试通常是可行的。当然,您应该使用 为可测试性设计类,这样就需要更少的反射。

例如 Spring 就有 ReflectionTestUtils,但是它的用途是设置依赖关系的模拟,在这里 Spring 应该注入这些依赖关系。

这个话题比“做与不做”更深入,而且认为 what应该被测试——对象的内部状态是否需要被测试; 我们是否应该对被测试的类的设计提出质疑; 等等。

我认为这是一个不好的做法,但是仅仅改变生产代码中的可见性并不是一个好的解决方案,您必须查看原因。要么你有一个不可测试的 API (也就是说这个 API 没有暴露足够的内容让测试去处理) ,所以你想要测试私有状态,要么你的测试与你的实现过于耦合,这将使它们在你重构的时候只有很少的用处。

在不了解更多关于您的案例的情况下,我不能说得更多,但是使用 反思确实被认为是一种不好的做法。就个人而言,我宁愿让测试成为被测类的一个静态内部类,也不愿求助于反射(如果说 API 的不可测部分不在我的控制之下的话) ,但是有些地方会有一个更大的问题,即测试代码与生产代码在同一个包中,而不是使用反射。

编辑: 作为对您编辑的回应,那个至少和使用反射一样糟糕,可能更糟。通常的处理方法是使用相同的包,但是将测试保存在单独的目录结构中。如果单元测试不属于被测类的同一个包,那么我不知道什么属于。

无论如何,你仍然可以通过使用 protected (不幸的是,不是 package-private,这是非常理想的)来解决这个问题,方法是测试一个子类,如下所示:

 public class ClassUnderTest {
protect void methodExposedForTesting() {}
}

And inside your unit test

class ClassUnderTestTestable extends ClassUnderTest {
@Override public void methodExposedForTesting() { super.methodExposedForTesting() }
}

如果你有一个受保护的构造函数:

ClassUnderTest test = new ClassUnderTest(){};

对于正常情况,我不一定推荐上面的方法,但是你被要求工作的限制已经不是“最佳实践”了。

我认为您的代码应该以两种方式进行测试。您应该通过单元测试测试您的公共方法,这将作为我们的黑盒测试。因为你的代码被分解成可管理的函数(好的设计) ,你会想用反射来单元测试这些单独的部分,以确保它们独立于过程工作,我能想到的唯一方法就是使用反射,因为它们是私有的。

至少,这是我在单元测试过程中的想法。

从测试驱动 设计的角度来看,这是一个不好的做法。我知道您没有标记这个 TDD,也没有具体询问它,但是 TDD 是一个很好的实践,而且这违背了它的原则。

在 TDD 中,我们使用测试来定义类的接口—— 公众人士接口——因此我们编写的测试只与公共接口直接交互。我们关心的是那个接口; 适当的访问级别是设计的重要组成部分,也是好代码的重要组成部分。如果你发现自己需要测试一些私人的东西,根据我的经验,通常是一种设计气味。

为了补充其他人所说的,考虑以下几点:

//in your JUnit code...
public void testSquare()
{
Class classToTest = SomeApplicationClass.class;
Method privateMethod = classToTest.getMethod("computeSquare", new Class[]{Integer.class});
String result = (String)privateMethod.invoke(new Integer(3));
assertEquals("9", result);
}

Here, we are using reflection to execute the private method SomeApplicationClass.computeSquare(), passing in an Integer and returning a String result. This results in a JUnit test which will compile fine but fail during execution if any of the following occur:

  • 方法名称“ computeSquare”被重命名
  • The method takes in a different parameter type (e.g. changing Integer to Long)
  • The number of parameters change (e.g. passing in another parameter)
  • 方法的返回类型改变(可能从 String 改为 Integer)

相反,如果没有简单的方法来证明 computeSquare 已经通过公共 API 完成了它应该完成的任务,那么您的类可能会尝试做很多事情。将此方法拖入一个新类中,该类将为您提供以下测试:

public void testSquare()
{
String result = new NumberUtils().computeSquare(new Integer(3));
assertEquals("9", result);
}

现在(特别是当你使用现代 IDE 中可用的重构工具时) ,改变方法的名称对你的测试没有影响(因为你的 IDE 也会重构 JUnit 测试) ,而改变参数类型、参数数量或者方法的返回类型会在你的 JUnit 测试中标记一个编译错误,这意味着你不会在一个编译但在运行时失败的 JUnit 测试中检查。

我的最后一点是,有时候,特别是在使用遗留代码并需要添加新功能时,在单独的可测试良好编写的类中这样做可能并不容易。在这个例子中,我的建议是将新的代码更改隔离到受保护的可见性方法中,这些方法可以直接在 JUnit 测试代码中执行。这允许您开始构建测试代码的基础。最终,您应该重构该类并提取所添加的功能,但与此同时,新代码上受保护的可见性有时可能是您在不进行重大重构的情况下实现可测试性的最佳方式。

我同意 Bozho 的观点:

It is really bad to modify the visibility of a production API just for the sake of testing.

But if you try to 做一些最简单的事情 then it might be preferred to writing Reflection API boilerplate, at least while your code is still new/changing. I mean, the burden of manually changing the reflected calls from your test every time you change the method name, or params, is IMHO too much burden and the wrong focus.

我陷入了这样一个陷阱: 仅仅为了测试而放松可访问性,然后不经意地从其他产品代码中访问 was-private 方法,我想到了 dp4j.jar: 它注入了反射 API 代码(龙目怪) ,这样你就不会改变产品代码,也不会自己编写反射 API; dp4j 在编译时用等效的反射 API 替代了单元测试中的直接访问。这是 带有 JUnit 的 dp4j 示例