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

这个问题在这里已有答案:

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

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

使用Reflection是不是很糟糕的做法?我真的不相信 -

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

回答(7)

2 years ago

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

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

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

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

2 years ago

仅仅为了测试而修改 生产环境 API的可见性是非常糟糕的 . 出于正当理由,该可见性可能被设置为其当前值,并且不会被更改 .

使用反射进行单元测试大多都很好 . 当然,你应该design your classes for testability,这样就不需要反射了 .

例如,Spring有 ReflectionTestUtils . 但它的目的是设置依赖的模拟, Spring 天应该注入它们 .

主题比"do & don't"更深,并且考虑应该测试的内容 - 是否需要测试对象的内部状态;我们是否应该负责质疑被测 class 的设计;等等

2 years ago

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

在TDD中,我们使用我们的测试来定义类的接口 - 公共接口 - 因此我们通常根据我的经验设计气味 .

2 years ago

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

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

编辑:响应您的编辑,至少是使用反射的做法很差,可能更糟糕 . 通常处理的方式是使用相同的包,但将测试保存在单独的目录结构中 . 如果单元测试不知道是什么 .

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

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

在你的单元测试中

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

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

ClassUnderTest test = new ClassUnderTest(){};

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

2 years ago

我想到了Bozho:

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

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

陷入了放松可访问性的陷阱只是为了测试,然后无意中从其他 生产环境 代码访问was-private方法我想到dp4j.jar:它注入了反射API代码(Lombok-style),这样你就不必自己编写Reflection API了 . dp4j在编译时使用等效的反射API替换单元测试中的直接访问 . 这是example of dp4j with JUnit .

2 years ago

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

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

2 years ago

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

//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测试代码中执行这些方法 . 这允许您开始构建测试代码的基础 . 最终,您应该重构该类并提取您添加的功能,但与此同时,对新代码的受保护可见性有时可能是您在没有重大重构的可测试性方面的最佳前进方式 .