簡體   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