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

时间:2010-05-11 13:40:04

标签: java unit-testing reflection

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

上周我不得不测试一些软件包,因此编写了一些JUnit测试。像往常一样,我使用Reflection来访问私有字段和方法。但我检查代码的主管对此并不满意,并告诉我,Reflection API并不适合用于此类“黑客攻击”。相反,他建议修改生产代码中的可见性。

使用Reflection真是不好的做法吗?我真的不相信 -

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

7 个答案:

答案 0 :(得分:66)

恕我直言反思应该只是最后的手段,保留用于单元测试遗留代码或您无法更改的API的特殊情况。如果您正在测试自己的代码,那么您需要使用Reflection意味着您的设计不可测试,因此您应该修复它而不是诉诸于反射。

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

请注意,使用Reflection通常会导致代码除了更难理解和维护之外,还会更脆弱。有一整套错误,在正常情况下会被编译器检测到,但是使用Reflection它们只会出现运行时异常。

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

答案 1 :(得分:63)

仅仅为了测试而修改生产API的可见性真的不好。该可见性可能由于正当理由而设置为其当前值,并且不得更改。

使用反射进行单元测试几乎没问题。当然,你应该design your classes for testability,这样就不需要反思了。

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

主题比“do& do not”更深入,并且要考虑应该测试什么 - 是否需要测试对象的内部状态;我们是否应该负责质疑被测班级的设计;等

答案 2 :(得分:29)

从TDD的角度来看 - 测试驱动设计 - 这是一种不好的做法。我知道你没有标记这个TDD,也没有特别询问它,但是TDD是一个很好的做法,这与它的结果不符。

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

答案 3 :(得分:7)

我认为这是一种不好的做法,但只是改变生产代码的可见性不是一个好的解决方案,你必须看看原因。要么你有一个不可测试的API(那就是API没有公开足以让测试得到一个句柄)所以你正在寻找测试私有状态,或者你的测试与你的实现太耦合,这将使它们成为重构时只能边际使用。

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

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

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

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

在单元测试中

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

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

ClassUnderTest test = new ClassUnderTest(){};

我不一定会在正常情况下推荐上述内容,但是要求您使用限制而不是“最佳做法”。

答案 4 :(得分:6)

我想到了Bozho:

  

仅仅为了测试而修改生产API的可见性真的很糟糕。

但是如果您尝试do the simplest thing that could possibly work,那么可能更喜欢编写Reflection API样板文件,至少在您的代码仍然是新的/更改时。我的意思是,每次更改方法名称或params时,手动更改测试反射调用的负担是恕我直言,太多负担和错误的焦点。

陷入了放松可访问性的陷阱,只是为了测试,然后无意中从我认为dp4j.jar的其他生产代码中访问了was-private方法:它注入了Reflection API代码(Lombok-style)所以您不更改生产代码并且不自己编写Reflection API; dp4j在编译时使用等效的反射API替换单元测试中的直接访问。这是一个example of dp4j with JUnit

答案 5 :(得分:4)

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

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

答案 6 :(得分:3)

要添加其他人所说的内容,请考虑以下事项:

//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(),传入一个Integer并返回一个String结果。这导致JUnit测试,如果发生以下任何一种情况,它将在编译期间正常编译但失败:

  • 方法名称“computeSquare”已重命名
  • 该方法采用不同的参数类型(例如,将Integer更改为Long)
  • 参数数量发生变化(例如传入另一个参数)
  • 方法的返回类型更改(可能从String更改为Integer)

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

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

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

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