2

很多时候,我参与了 API 的设计/实现,我面临着这个困境。

我是信息隐藏的坚定支持者,并尝试为此使用各种技术,包括但不限于内部类、私有方法、包私有限定符等。

这些技术的问题在于它们往往会妨碍良好的可测试性。虽然其中一些技术可以解决(例如,通过将一个类放入同一个包中的包私有性),但其他技术并不那么容易解决,并且需要反射魔法或其他技巧。

我们来看一个具体的例子:

public class Foo {
   SomeType attr1;
   SomeType attr2;
   SomeType attr3;

   public void someMethod() {
      // calculate x, y and z
      SomethingThatExpectsMyInterface something = ...;
      something.submit(new InnerFoo(x, y, z));
   }

   private class InnerFoo implements MyInterface {
      private final SomeType arg1;
      private final SomeType arg2;
      private final SomeType arg3;

      InnerFoo(SomeType arg1, SomeType arg2, SomeType arg3) {
         this.arg1 = arg1;
         this.arg2 = arg2;
         this.arg3 = arg3;
      }

      @Override
      private void methodOfMyInterface() {
         //has access to attr1, attr2, attr3, arg1, arg2, arg3
      }
   }
}

有充分的理由不公开InnerFoo- 没有其他类,库应该可以访问它,因为它没有定义任何公共合同,并且作者故意不希望它可以访问。然而,为了使其 100% TDD-kosher 并且无需任何反射技巧即可访问,InnerFoo应该像这样重构:

private class OuterFoo implements MyInterface {
   private final SomeType arg1;
   private final SomeType arg2;
   private final SomeType arg3;
   private final SomeType attr1;
   private final SomeType attr2;
   private final SomeType attr3;

   OuterFoo(SomeType arg1, SomeType arg2, SomeType arg3, SomeType attr1, SomeType attr2, SomeType attr3) {
      this.arg1 = arg1;
      this.arg2 = arg2;
      this.arg3 = arg3;
      this.attr1 = attr1;
      this.attr2 = attr2;
      this.attr3 = attr3;
   }

   @Override
   private void methodOfMyInterface() {
      //can be unit tested without reflection magic
   }
}

这个例子只涉及 3 个 attrs,但有 5-6 个是相当合理的,OuterFoo然后构造函数必须接受 8-10 个参数!在顶部添加 getter,您已经有 100 行完全无用的代码(还需要 getter 来获取这些 attrs 进行测试)。是的,我可以通过提供构建器模式使情况变得更好,但我认为这不仅是过度设计,而且违背了 TDD 本身的目的!

这个问题的另一个解决方案是为 class 公开一个受保护的方法Foo,将其扩展FooTest并获取所需的数据。同样,我认为这也是一种不好的方法,因为protected方法确实定义了一个合同,并且通过公开它,我现在已经隐式地签署了它。

不要误会我的意思。我喜欢编写可测试的代码我喜欢简洁、干净的 API、短代码块、可读性等。但我不喜欢在信息隐藏方面做出任何牺牲,因为它更容易进行单元测试

任何人都可以对此提供任何想法(总的来说,特别是)?对于给定的示例,还有其他更好的解决方案吗?

4

6 回答 6

4

我对这类事情的首选答案是“测试代理”。在您的测试包中,从您的被测系统派生一个子类,其中包含对受保护数据的“直通”访问器。

优点:

  • 您可以直接测试或模拟您不想公开的方法。
  • 由于测试代理存在于测试包中,您可以确保它永远不会在生产代码中使用。
  • 与直接测试类相比,测试代理需要对代码进行更少的更改以使其可测试。

缺点:

  • 该类必须是可继承的(否final
  • 您需要访问的任何隐藏成员都不能是私有的;保护是你能做的最好的。
  • 这不是严格意义上的 TDD。TDD 将适用于首先不需要测试代理的模式。
  • 这甚至不是严格意义上的单元测试,因为在某种程度上,您依赖于代理和实际 SUT 之间的“集成”。

简而言之,这通常应该是罕见的。我倾向于仅将它用于 UI 元素,其中最佳实践(以及许多 IDE 的默认行为)是将嵌套的 UI 控件声明为无法从类外部访问。绝对是个好主意,因此您可以控制调用者如何从 UI 获取数据,但这也使得难以为控件提供一些已知值来测试该逻辑。

于 2011-02-17T17:04:57.443 回答
3

我认为您应该重新考虑使用反射。

它有自己的缺点,但如果它允许您在没有虚拟代码的情况下维护所需的安全模型,那可能是一件好事。反射通常不是必需的,但有时没有好的替代品。

信息隐藏的另一种方法是将类/对象视为黑匣子,不访问任何非公共方法(尽管这可能允许测试因“错误”原因而通过,即答案是正确的但出于错误的原因。)

于 2011-02-17T17:27:05.907 回答
2

我看不出信息隐藏是如何降低你的可测试性的。

如果您在此方法中注入SomethingThatExpectsMyInterfaceused 而不是直接构造它:

public void someMethod() {
   // calculate x, y and z
   SomethingThatExpectsMyInterface something = ...;
   something.submit(new InnerFoo(x, y, z));
}

然后在单元测试中,您可以将模拟版本注入这个类,SomethingThatExpectsMyInterface并轻松断言当您someMethod()使用不同的输入调用时会发生什么 -mockSomething接收某些值的参数。

我认为你可能已经过度简化了这个例子,因为如果 InnerFooSomethingThatExpectsMyInterface接收到它的类型的参数,它就不能是一个私有类。

“信息隐藏”并不一定意味着您在类之间传递的对象需要保密 - 只是您不需要使用此类的外部代码了解此类的细节InnerFoo或其他细节与他人交流。

于 2011-02-17T17:05:34.200 回答
2

SomethingThatExpectsMyInterface可以在外面测试Foo,对吧?您可以submit()使用您自己的实现了MyInterface. 因此,该单元得到了照顾。现在您正在Foo.someMethod()使用经过良好测试的单元和未经测试的内部类进行测试。这并不理想——但也不算太糟糕。当你试驾someMethod()时,你隐含地在试驾内部类。我知道按照一些严格的标准,这不是纯粹的TDD,但我认为这已经足够了。除了满足失败的测试之外,您不会编写内部类的一行;在您的测试和测试代码之间存在单一级别的间接性并不构成大问题。

于 2011-02-17T17:33:12.117 回答
0

在您的示例中,看起来 Foo 类确实需要一个协作者InnerFoo。

在我看来,信息隐藏和可测试性之间的紧张关系可以通过“复合比其部分的总和更简单”的座右铭来解决。

Foo 是复合材料的外观(在您的情况下只是 InnerFoo,但无关紧要。)外观对象应根据其预期行为进行测试。如果您觉得 InnerFoo 对象代码不足以通过对 Foo 行为的测试来驱动,您应该考虑 InnerFoo 在您的设计中代表什么。可能是你错过了一个设计概念。当你找到它、命名它并定义它的职责时,你可以单独测试它的行为。

于 2011-03-06T10:23:52.827 回答
-1

我不喜欢在信息隐藏方面做出任何牺牲

首先,使用 Python 工作几年。

private不是特别有用。它使类的扩展变得困难,并且使测试变得困难。

考虑重新考虑你对“隐藏”的立场。

于 2011-02-17T20:25:00.910 回答