【问题标题】:Is it bad practice to use Reflection in Unit testing? [duplicate]在单元测试中使用反射是不好的做法吗? [复制]
【发布时间】:2011-02-18 03:53:47
【问题描述】:

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

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

使用反射真的很糟糕吗?我真的不敢相信-

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

【问题讨论】:

  • 您使用反射、设置、获取或两者都练习哪种访问方式?
  • @fish:我只使用 get 来验证是否设置了某些特定值
  • Java 中的正常做法是将测试放在同一个包中,以便您可以测试“包私有”(默认访问)类。
  • 对上一条评论的一个补充(关于将测试放在同一个包中)是您将测试放在一个单独的源代码树中,它没有与应用程序一起部署。
  • 为这些变量添加 getter 应该没什么大不了的,特别是如果你可以说服你的主管接受不同源代码树但在同一个包中的测试(这已经是常见的做法提及)。恕我直言,直接访问字段总是一个坏主意,无论是使用反射还是仅仅具有松散的可见性。

标签: java unit-testing reflection


【解决方案1】:

我赞同博卓的想法:

仅仅为了测试而修改生产 API 的可见性是非常糟糕的。

但是,如果您尝试使用 do the simplest thing that could possibly work,那么它可能比编写反射 API 样板文件更可取,至少在您的代码仍然是新的/正在更改的时候。我的意思是,每次更改方法名称或参数时手动更改测试中的反射调用的负担,恕我直言,负担过重且关注点错误。

陷入了仅为测试而放松可访问性的陷阱,然后无意中从我想到的其他生产代码中访问了 was-private 方法dp4j.jar:它注入了反射 API 代码 (Lombok-style),这样你就不会' t 更改生产代码并且不要自己编写反射 API; dp4j 在编译时将您在单元测试中的直接访问替换为等效的反射 API。这是example of dp4j with JUnit

【讨论】:

    【解决方案2】:

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

    //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 测试代码中执行这些更改。这使您可以开始构建测试代码库。最终,您应该重构该类并提取您添加的功能,但与此同时,新代码的受保护可见性有时可能是您在无需进行重大重构的情况下提高可测试性的最佳方式。

    【讨论】:

      【解决方案3】:

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

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

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

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

      【讨论】:

      • 反射在测试框架中对于调用测试和模拟接口显然很有用,作为编译时注释处理器的替代方案。但是帽子应该被明确指出是一种单独的用法。
      • @tackline,这是我真正的意思,感谢您指出。我在回答中添加了说明。
      • 你不需要很多限定符。反射主要应作为最后的手段使用。这使得对程序的推理实际上变得不可能。
      【解决方案4】:

      从 TDD 的角度来看 - 测试驱动设计 - 这是一种不好的做法。我知道您没有标记此 TDD,也没有专门询问它,但 TDD 是一种很好的做法,这违背了它的原则。

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

      【讨论】:

      • 同意。将正在测试的对象视为黑盒 - 确保输出与输入匹配。如果他们不知道,你知道你在实现的内部有问题。
      • 但是测试对象状态呢?
      • 对象的内部状态无关紧要,只要其外部行为正确即可。
      【解决方案5】:

      我认为这是一种不好的做法,但仅更改生产代码中的可见性并不是一个好的解决方案,您必须查看原因。要么你有一个不可测试的 API(即 API 没有暴露足够的测试来处理),所以你正在寻找测试私有状态,或者你的测试与你的实现太耦合,这将使它们仅在重构时边缘使用。

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

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

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

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

      在你的单元测试中

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

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

      ClassUnderTest test = new ClassUnderTest(){};
      

      对于正常情况,我不一定推荐上述方法,但您被要求遵守的限制条件并不是“最佳实践”。

      【讨论】:

      • +1 改变生产代码的可见性不是一个好的解决方案
      • @Yishai 但是如果我们在本地和依赖 jar 中都有相同的类名问题怎么办?如何在 Java gradle 中测试受保护的方法?在这方面需要帮助。
      【解决方案6】:

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

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

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

      主题比“do & don't”更深,关于what应该测试——对象的内部状态是否需要测试;我们是否应该对被测类的设计提出质疑;等等

      【讨论】:

      • 你能解释一下你的反对意见吗?
      • 生产 API 的变化有两个方面。仅为测试更改它是不好的,而更新您的 API/设计以使您的代码更具可测试性(因此您不会仅为测试更改可见性)是一个好习惯。
      • 如果您确定您所做的事情没有副作用 - 是的。 ;)
      • 如果您没有测试来测试您所做的 api 更改,因此您可以编写测试...... Bozho 有一个非常有效的观点和答案。
      • 但请注意,将成员的可见性从 private 更改为 package-private 对您的包的消费者没有任何影响,除非他们在您的包的命名空间中编写代码。这种对范围的特殊更改可能是可以容忍的。
      【解决方案7】:

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

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

      【讨论】:

        猜你喜欢
        • 2013-01-14
        • 1970-01-01
        • 1970-01-01
        • 2014-03-11
        • 2013-08-13
        • 2020-03-25
        • 2012-08-06
        • 2010-10-20
        • 1970-01-01
        相关资源
        最近更新 更多