关于java:在单元测试中使用Reflection是不好的做法吗?

Is it bad practice to use Reflection in Unit testing?

本问题已经有最佳答案,请猛点这里访问。

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

上周我不得不测试一些包,因此写了一些JUnit测试。和往常一样,我使用反射访问私有字段和方法。但是我的负责检查代码的主管对此并不满意,他告诉我反射API并不打算用于这种"黑客攻击"。相反,他建议修改生产代码中的可见性。

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

编辑:我应该提到我被要求所有的测试都在一个称为测试的单独的包中(所以使用受保护的可视性,例如,也不是一个可能的解决方案)


IMHO反射实际上应该是最后一种手段,专门用于单元测试遗留代码或无法更改的API的特殊情况。如果您正在测试自己的代码,那么您需要使用反射这一事实意味着您的设计是不可测试的,因此您应该修复它,而不是使用反射。

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

请注意,在一般情况下使用反射会导致代码变得更难理解和维护,而且代码也更脆弱。在正常情况下,编译器会检测到一整套错误,但经过反射后,它们只会出现运行时异常。

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


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

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

例如,弹簧有ReflectionTestUtils。但它的目的是设置依赖性的模拟,Spring应该在其中注入依赖性。

这个主题比"做与不做"更深入,并且考虑到应该测试什么——对象的内部状态是否需要测试;我们是否应该质疑被测试类的设计;等等。


从TDD(测试驱动设计)的角度来看,这是一个糟糕的实践。我知道你没有贴上这个TDD标签,也没有特别询问它,但是TDD是一个很好的实践,这违背了它的本质。

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


我认为这是一个糟糕的实践,但是仅仅改变生产代码中的可见性并不是一个好的解决方案,您必须看看原因。要么您有一个不稳定的API(即API没有足够的公开来让测试获得句柄),所以您希望测试私有状态,要么您的测试与您的实现耦合得太多,这将使它们在重构时只具有边际用途。

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

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

无论如何,通过测试这样的子类,您仍然可以通过使用protected(不幸的是不是package private,这是非常理想的)来解决这个问题:

1
2
3
 public class ClassUnderTest {
      protect void methodExposedForTesting() {}
 }

在单元测试中

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

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

1
ClassUnderTest test = new ClassUnderTest(){};

在正常情况下,我并不一定推荐上述方法,但要求您遵循的限制条件,而不是"最佳实践"。


我第二个想到博佐:

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

但是,如果您尝试做最简单的工作,那么它可能会比编写反射API样板文件更好,至少在您的代码仍然是新的/正在更改的时候。我的意思是,每次更改方法名或参数时,手动更改测试中反射的调用的负担是太大的负担和错误的焦点。

我陷入了一个只为测试而放松可访问性的陷阱,然后无意中从其他生产代码访问was private方法,我想到了dp4j.jar:它注入反射API代码(lombok样式),这样您就不会更改生产代码,也不会自己编写反射API;dp4j取代了您的直接访问I。n编译时使用等效反射API进行的单元测试。下面是一个带有JUnit的DP4J示例。


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

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


要补充其他人所说的内容,请考虑以下内容:

1
2
3
4
5
6
7
8
//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);
}

这里,我们使用反射来执行私有方法someapplicationclass.computesquare(),传入一个整数并返回一个字符串结果。这将导致JUnit测试编译良好,但在执行过程中如果发生以下任何情况,则会失败:

  • 方法名"computesquare"已重命名
  • 该方法采用不同的参数类型(例如,将integer更改为long)
  • 参数数目发生变化(例如传入另一个参数)
  • 方法的返回类型更改(可能从字符串更改为整数)

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

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

现在(尤其是当您使用现代IDE中可用的重构工具时),更改方法的名称对测试没有影响(因为您的IDE也将重构JUnit测试),同时更改参数类型、参数数量或方法的返回类型将在JUnit测试中标记编译错误,这意味着您不会正在签入JUnit测试,该测试编译但在运行时失败。

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