2010-11-10 133 views
7

假設一個傳統的類和方法的結構像下面單元測試和依賴注入深度嵌套依賴

public class Foo 
{ 
    public void Frob(int a, int b) 
    { 
     if (a == 1) 
     { 
      if (b == 1) 
      { 
       // does something 
      } 
      else 
      { 
       if (b == 2) 
       { 
        Bar bar = new Bar(); 
        bar.Blah(a, b); 
       } 
      } 
     } 
     else 
     { 
      // does something 
     } 
    } 
} 

public class Bar 
{ 
    public void Blah(int a, int b) 
    { 
     if (a == 0) 
     { 
      // does something 
     } 
     else 
     { 
      if (b == 0) 
      { 
       // does something 
      } 
      else 
      { 
       Baz baz = new Baz(); 
       baz.Save(a, b); 
      } 
     } 
    } 
} 

public class Baz 
{ 
    public void Save(int a, int b) 
    { 
     // saves data to file, database, whatever 
    } 
} 

然後承擔管理髮出含糊不清的任務,因爲我們做的每一個新事物進行單元測試,是它的一個添加功能,修改要求或錯誤修復。

我可能是字面解釋的堅持者,但我認爲「單元測試」這個詞意味着什麼。例如,這並不意味着給定1和2的輸入,即Foo.Frob的單元測試只有在將1和2保存到數據庫時纔會成功。基於我讀過的內容,我相信它最終意味着基於1和2的輸入,Frob被調用Bar.BlahBar.Blah是否做了它應該做的事情並不是我的直接擔憂。如果我關心測試整個過程,我相信還有另一個術語,對吧?功能測試?情景測試?隨你。請糾正我,如果我太僵硬,請!

與我的時刻嚴格解釋堅持,讓我們假設我想嘗試使用依賴注入,有一個好處是,我可以嘲笑了我的班,這樣我可以,例如,堅持我的測試數據到數據庫或文件或任何情況下。在這種情況下,Foo.Frob需要IBar,IBar需要IBaz,IBaz可能需要數據庫。這些依賴關係在哪裏被注入?分成Foo?或者Foo只需要IBar,然後Foo負責創建IBaz的實例?

當你進入像這樣的嵌套結構時,可以很快看到可能存在多個必需的依賴關係。執行此類注射的首選或可接受的方法是什麼?

回答

7

讓我們從您的最後一個問題開始。注入依賴關係的位置:常用的方法是使用構造函數注入(如described by Fowler)。因此Foo在構造函數中注入了IBarIBarBar的具體實現反過來將IBaz注入到它的構造函數中。最後IBaz執行(Baz)有一個IDatabase(或其他)注入。如果您使用DI框架(例如Castle Project),則只需要讓DI容器爲您解析Foo的實例即可。然後,它將使用您已配置的任何內容來確定您正在使用哪種實施方式IBar。如果確定您的IBar實施Bar它就會決定你使用等,它們的IBaz實施

什麼這種方法給你,是你可以測試每個孤立的具體實現,而只是檢查它正確調用(嘲諷)抽象。

要評論你對過於僵硬等問題的評論,我唯一能說的是,我認爲你正在選擇正確的道路。也就是說,當實施所有這些測試的實際成本對他們顯而易見時,管理層可能會感到意外。

希望這會有所幫助。

+0

+1 - 對於深度嵌套的依賴關係,我相信IoC/DI Container框架通常是一個很好的解決方案。請參閱http://www.hanselman.com/blog/ListOfNETDependencyInjectionContainersIOC。aspx – TrueWill 2010-11-10 17:50:25

2

我不認爲有一個「首選」的方法來解決這個問題,但您的主要擔心之一似乎是依賴注入,當您創建Foo時,您還需要創建Baz,這可能是不必要的。一個簡單的方法是,Bar不直接依賴於IBaz,而是依賴於Lazy<IBaz>Func<IBaz>,允許您的IoC容器在不立即創建Baz的情況下創建Bar的實例。

例如:

public interface IBar 
{ 
    void Blah(int a, int b); 
} 

public interface IBaz 
{ 
    void Save(int a, int b); 
} 

public class Foo 
{ 
    Func<IBar> getBar; 
    public Foo(Func<IBar> getBar) 
    { 
     this.getBar = getBar; 
    } 

    public void Frob(int a, int b) 
    { 
     if (a == 1) 
     { 
      if (b == 1) 
      { 
       // does something 
      } 
      else 
      { 
       if (b == 2) 
       {       
        getBar().Blah(a, b); 
       } 
      } 
     } 
     else 
     { 
      // does something 
     } 
    } 
} 



public class Bar : IBar 
{ 
    Func<IBaz> getBaz; 

    public Bar(Func<IBaz> getBaz) 
    { 
     this.getBaz = getBaz; 
    } 

    public void Blah(int a, int b) 
    { 
     if (a == 0) 
     { 
      // does something 
     } 
     else 
     { 
      if (b == 0) 
      { 
       // does something 
      } 
      else 
      { 
       getBaz().Save(a, b); 
      } 
     } 
    } 
} 

public class Baz: IBaz 
{ 
    public void Save(int a, int b) 
    { 
     // saves data to file, database, whatever 
    } 
} 
1

我說你說得對單元測試,它應該涵蓋的代碼一個相當小的「單位」,但到底有多少是爲辯論。但是,如果它觸及數據庫,那幾乎肯定不是單元測試 - 我會稱之爲集成測試。

當然,這可能是'管理'並不真正關心這樣的事情,並且會非常滿意集成測試!它們仍然是完全有效的測試,並且可能更易於添加,儘管不一定會導致像單元測試一樣更好的設計。

但是,當你的IBar被創建後注入你的IBaz,然後將你的IBar注入你的Foo。這可以在構造函數或setter中完成。構造函數更好(IMO),因爲它只會導致創建有效的對象。你可以做的一個選擇(稱爲窮人的DI)是重載構造函數,所以你可以傳入一個IBar進行測試,並在代碼中使用的無參數構造函數中創建一個Bar。您失去了良好的設計優勢,但值得考慮。

當您完成所有工作後,請嘗試一個IoC容器,例如Ninject,這可能會讓您的生活更輕鬆。 (也可以考慮諸如TypeMockMoles之類的工具,它們可以在沒有界面的情況下嘲笑事物 - 但要記住這是作弊行爲,並且不會得到改進的設計,所以應該是最後的手段)。

0

當您深層嵌套的層次結構出現問題時,它意味着您沒有注入足夠的依賴關係。

這裏的問題是,我們有巴茲,它看起來像你需要通過巴茲到美孚誰傳遞給巴爾最終調用它的方法。這看起來像很多工作,有點沒用...

你應該做的是傳遞巴茲作爲酒吧對象的構造函數的參數。然後吧應該傳遞給Foo對象的構造函數。 Foo絕對不應該碰到甚至不知道Baz的存在。只有酒吧關心巴茲。在測試Foo時,您可以使用Bar界面的另一個實現。這個實現可能只是記錄了Blah被調用的事實。它不需要考慮巴茲的存在。

你可能會想是這樣的:

class Foo 
{ 
    Foo(Baz baz) 
    { 
     bar = new Bar(baz); 
    } 

    Frob() 
    { 
     bar.Blah() 
    } 
} 

class Bar 
{ 
    Bar(Baz baz); 

    void blah() 
    { 
      baz.biz(); 
    } 
} 

你應該做這樣的事情:

class Foo 
{ 
    Foo(Bar bar); 

    Frob() 
    { 
     bar.Blah() 
    } 
} 

class Bar 
{ 
    Bar(Baz baz); 

    void blah() 
    { 
      baz.biz(); 
    } 
} 

如果你這樣做是正確,每個對象應該只需要對付的對象也直接與之交互。

在您的實際代碼中,您可以即時構建對象。要做到這一點,你只需要傳遞BarFactory和BazFactory的實例來在需要時構造對象。基本原則保持不變。

0

聽起來有一點掙扎的在這裏:

  1. 處理遺留代碼
  2. 繼續寫維護的代碼
  3. 測試你的代碼(2延續真)
  4. ,也,我想,釋放一些東西

管理層使用「單元測試」只能通過詢問他們來確定,但她e就我的2c而言,關於上述所有4個問題,這裏可能是一個好主意。

我認爲Frob被調用Bar.Blah並且Bar.Blah做它應該做的事情都是很重要的。當然,這些是不同的測試,但爲了發佈無bug(或儘可能少的bug)軟件,您確實需要進行單元測試(Frob,調用Bar.Blah)以及集成測試(Bar.Blah做了什麼應該做的)。如果你也可以單元測試Bar.Blah,那將是非常好的,但如果你不希望改變,那麼它可能不會太有用。

當然,你會希望每次發現bug時都要添加單元測試,最好是在修復之前。通過這種方式,您可以確保在修復之前測試中斷,然後修復導致測試通過。

你不想整天花費重構或重寫代碼庫,所以你需要明白你如何去處理依賴關係。在Foo的示例中,您最好將Bar升至internal property,並設置項目以使內部對您的測試項目可見(使用AssemblyInfo.cs中的InternalsVisibleTo屬性)。 Bar的默認構造函數可以將該屬性設置爲new Bar()。您的測試可以將其設置爲用於測試的Bar的某個子類。或者一個存根。我認爲這將減少你必須做出的變化量,使這件事情可以進行下去。

當然,除非您對該類進行其他更改,否則不需要對類進行任何重構。

2

您在文章的第一部分中描述的測試類型(當您嘗試將所有零件放在一起時)通常將其定義爲集成測試。作爲解決方案的良好實踐,您應該有單元測試項目和集成測試項目。爲了在你的代碼中注入依賴,第一個也是最重要的規則是使用接口進行編碼。假設這個,假設你的類包含一個接口作爲成員,並且你想要注入/模擬它:你可以將它作爲一個屬性公開或者使用類構造函數傳遞實現。 我更喜歡使用屬性來公開依賴關係,這樣構造函數不會變得太冗長。 我建議你使用NUnit或MbUnit的一個測試框架,起訂量爲模擬框架(在它的輸出更清晰比犀牛嘲笑) 下面是關於如何用起訂量嘲笑一個一些例子http://code.google.com/p/moq/wiki/QuickStart

希望它的文檔幫助

+0

我必須說我從來沒有構造函數變得冗長的問題。我幾乎沒有超過四個類的依賴關係。也許你的課程做得太多,你需要重構。此外,將依賴關係放在構造函數上使得依賴關係非常清晰。 – Steven 2010-11-10 19:52:12

+0

+1將集成測試放入他們自己的項目中。 – Steven 2010-11-10 19:53:41