繁体   English   中英

编写/实现API:可测试性与信息隐藏

[英]Writing/implementing an API: testability vs information hiding

很多时候,我参与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本身的目的!

此问题的另一个解决方案是为Foo类公开受保护的方法,在FooTest扩展它并获取所需的数据。 同样,我认为这也是一个糟糕的方法,因为protected方法确实定义了一个契约,并且通过暴露它我现在已经隐式签署了它。

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

任何人都可以对此提出任何想法(一般而言,特别是)? 对于给定的例子,还有其他更好的解决方案吗?

我对这类事情的回答是“测试代理”。 在您的测试包中,从您正在测试的系统派生一个子类,该子类包含受保护数据的“传递”访问器。

好处:

  • 您可以直接测试或模拟您不希望公开的方法。
  • 由于测试代理位于测试包中,因此可以确保它永远不会在生产代码中使用。
  • 测试代理对代码的更改要少得多,以使其比您直接测试类时更易于测试。

缺点:

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

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

我认为你应该重新考虑使用反思。

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

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

我不知道信息隐藏在抽象中是如何降低可测试性的。

如果您正在注入此方法中使用的SomethingThatExpectsMyInterface而不是直接构造它:

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

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

我认为你可能过度简化了这个例子,因为如果SomethingThatExpectsMyInterface接收到其类型的参数,则InnerFoo不能是私有类。

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

SomethingThatExpectsMyInterface可以在Foo外面测试,对吧? 您可以使用自己的实现MyInterface的测试类调用其submit()方法。 所以这个单位得到了照顾。 现在,您正在使用经过充分测试的单元和未经测试的内部类测试Foo.someMethod() 这并不理想 - 但也不算太糟糕。 在测试驱动someMethod() ,您隐式地测试驱动内部类。 根据一些严格的标准,我知道这不是纯粹的 TDD,但我认为这已经足够了。 除了满足失败的测试之外,你不是在编写内部类的一行; 在测试和测试代码之间存在单一级别的间接不会构成大问题。

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

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

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

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

首先,使用Python几年。

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM