2009-08-31 145 views
0

首先,我要說的是,儘管是TDD的一名相當新的從業者,但我幾乎可以從中受益。我覺得我已經有足夠的進步來考慮使用嘲諷,並且在瞭解嘲笑與OOP適合的地方時碰到了一面真正的磚牆。關於嘲諷的另一個問題

我讀過儘可能多的相關文章/文章,我可以找到(Fowler,Miller),我仍然不完全清楚如何或何時模擬。

讓我舉一個具體的例子。我的應用程序有一個服務層類(有些人稱之爲應用層?),其中方法大致映射到特定用例。這些類可以與持久層,域層甚至其他服務類協作。我是一個善良的小男孩DI和已正確分解出來我的依賴,使他們能夠底塗用於測試目的等

樣本服務類可能是這樣的:

public class AddDocumentEventService : IAddDocumentEventService 
{ 
    public IDocumentDao DocumentDao 
    { 
     get { return _documentDao; } 
     set { _documentDao = value; } 
    } 
    public IPatientSnapshotService PatientSnapshotService 
    { 
     get { return _patientSnapshotService; } 
     set { _patientSnapshotService = value; } 
    } 

    public TransactionResponse AddEvent(EventSection eventSection) 
    { 
     TransactionResponse response = new TransactionResponse(); 
     response.Successful = false; 

     if (eventSection.IsValid(response.ValidationErrors)) 
     { 

      DocumentDao.SaveNewEvent(eventSection, docDataID); 

      int patientAccountId = DocumentDao.GetPatientAccountIdForDocument(docDataID); 
      int patientSnapshotId =PatientSnapshotService.SaveEventSnapshot(patientAccountId, eventSection.EventId); 

      if (patientSnapshotId == 0) 
      { 
       throw new Exception("Unable to save Patient Snapshot!"); 
      } 

      response.Successful = true; 
     } 
     return response; 
    } 

}

我使用NMock完成了對其方法(DocumentDao,PatientSnapshotService)進行隔離測試的過程。這裏的測試看起來像

[Test] 
public void AddEvent() 
    { 
     Mockery mocks = new Mockery(); 
     IAddDocumentEventService service = new AddDocumentEventService(); 
     IDocumentDao mockDocumentDao = mocks.NewMock<IDocumentDao>(); 
     IPatientSnapshotService mockPatientSnapshot = mocks.NewMock<IPatientSnapshotService>(); 

     EventSection eventSection = new EventSection(); 

     //set up our mock expectations 
     Expect.Once.On(mockDocumentDao).Method("GetPatientAccountIdForDocument").WithAnyArguments(); 
     Expect.Once.On(mockPatientSnapshot).Method("SaveEventSnapshot").WithAnyArguments(); 
     Expect.Once.On(mockDocumentDao).Method("SaveNewEvent").WithAnyArguments(); 

     //pass in our mocks as dependencies to the class under test 
     ((AddDocumentEventService)service).DocumentDao = mockDocumentDao; 
     ((AddDocumentEventService)service).PatientSnapshotService = mockPatientSnapshot; 

     //call the method under test 
     service.AddEvent(eventSection); 

     //verify that all expectations have been met 
     mocks.VerifyAllExpectationsHaveBeenMet(); 
    } 

我對這個小涉足嘲諷的想法是什麼如下:

  1. 這個測試的出現打破許多基本OO戒律,而不是其中最重要的就是封裝:我測試深入瞭解被測試類的具體實現細節(即方法調用)。每當課程內部發生變化時,我都會發現很多無用的時間用於更新測試。
  2. 也許是因爲我的服務類目前相當簡單,但我不太明白這些測試增加了什麼價值。是否我保證協作對象正在按照特定用例的要求被調用?代碼重複似乎很荒謬,因爲這樣一個小小的好處。

我錯過了什麼?

+0

「我缺少什麼?」 - 測試重構以消除冗餘代碼?即將常用步驟移到您的測試設置中。 – tvanfosson 2009-08-31 18:11:18

回答

2

你提到了一個很好的帖子,來自martin fowler關於這個問題。他提到的一點是,仿冒者是喜歡測試行爲並孤立東西的人。

傳統的TDD風格是使用真實物體,如果可能的話使用真實物體,如果使用真實物體時使用雙重物體,那麼傳統的TDDer會使用真正的倉庫和雙倍的郵件服務。沒有真正的問題太多。

一個mockist TDD從業者,但是,將始終使用一個模擬與有趣的行爲的任何對象。在這種情況下,對倉庫和郵件服務並重。

如果你不喜歡這種東西,你可能是一個經典的TDDer,只有當它使用mock時很尷尬(像郵件服務,或收取信用卡)。 否則,您創建自己的雙打(如創建內存數據庫)。

特別是,我是一個模擬器,但是我不確定是否調用了特定的方法(除非它不返回值)。無論如何,我會測試接口。當函數返回時,我使用模擬框架來創建存根。

最後,這一切都在你想要測試什麼以及如何測試。你認爲檢查這些方法是否真的被稱爲(使用模擬)很重要嗎?您是否想在通話前後檢查狀態(使用假貨)? 選擇足夠的東西來考慮它正在工作,然後構建你的測試來檢查它!

關於測試的價值,我有一些意見:

  • 在短期內,當你TDD你通常得到一個更好的設計,雖然你可能會需要更長的時間。
  • 在長期,你也不會太害怕改變和維護該代碼後(當你不會記得很好的細節),你會得到一個紅色的馬上,幾乎即時反饋

順便說一句,測試代碼大小與生產代碼大小一樣大是正常的。

+0

「[snip]我不確定是否調用了特定的方法(除非它不返回值)。無論如何,我會測試接口。[snip]」 我猜我沒有認爲測試方法調用的對象的存根/嘲諷是可選的。例如,在上面提供的示例場景中,如果我沒有爲膠合對象提供存根或實際實現,測試不會失敗嗎? – 2009-09-04 00:18:06

0

我發現這些類型的測試有一個假的(內存)持久層是有用的;然後,而不是驗證是否進行了某些調用,您可以驗證最終結果(項目現在存在於存儲庫中)。我知道你正在使用mock,但我想我說我不認爲這是最好的地方。

舉個例子,僞碼,這個測試我會看到如下:

Instantiate the fake repositories. 
Run your test method. 
Check the fake repository to see if the new elements exist in it. 

這樣可以使你的測試,不知道實現細節的。然而,它的確意味着維護假的持久層,所以我認爲這是一個折衷,你必須考慮。

1

打破封裝,從而使您的測試更緊密地耦合到您的代碼可以肯定是使用模擬的缺點。你不想讓你的測試對重構變得脆弱。這是你必須走的一條細線。我個人避免使用mock,除非它非常困難,尷尬或緩慢。看看你的代碼,首先,我會使用BDD風格:你的測試方法應該測試方法的特定行爲,並且應該這樣命名(可能類似於AddEventShouldSaveASnapshot)。其次,經驗法則是隻驗證預期的行爲發生,而不是編目應該發生的每一個方法調用。

+0

聽起來很有趣。我如何驗證我包含的樣本中的預期行爲,而不爲對象的合作者提供嘲諷?就像克里斯在下面的答案中所描述的一樣? – 2009-09-04 00:13:38

0

在代碼中分離利益相關者有時是值得的。

封裝是關於以最高的變化傳播概率最小化潛在依賴關係的數量。這是基於靜態源代碼。

單元測試是關於確保運行時行爲不會無意中更改:它不基於靜態源代碼。

當單元測試人員不是針對原始封裝的源代碼,而是將其所有私有訪問者自動更改爲公共訪問者的源代碼副本時(這只是一個四行shell腳本)。

這乾淨地分開encapsuation和單元測試。

那麼在你的單元測試中只剩下你有多低:你想測試多少種方法。這是一個品味問題。

更多關於封裝(但沒有單元測試),請參閱: http://www.edmundkirwan.com/encap/overview/paper7.html

1

與嘲笑應該幫助您瞭解合作對象—描述他們應該如何相互通信的協議之間的關係的測試。在這種情況下,您想知道事件到達時會有幾件事情會持續下去。如果你正在描述一個對象的外部關係,這並不是破壞封裝。 DAO和服務類型描述這些外部服務,但不定義它們如何實現。

這只是一個小例子,但代碼感覺程序而不是OO。有幾種簡單的值從一個對象中提取並傳遞給另一個對象的情況。也許某種Patient對象應該直接處理事件。很難說,但也許測試暗示這些設計問題?

在此期間,如果你不介意的自我宣傳和可以再等一個月, http://www.growing-object-oriented-software.com/

1

我有同樣不安的感覺,當我寫這樣的測試。當我通過將期望值複製到函數體中來實現函數時(特別是當我使用LeMock進行模擬時),它尤其讓我感到震驚。

但它確定。它發生了。此測試現在記錄並驗證被測系統如何與其依賴關係交互,這是一件好事。此測試還有其他問題:

  1. 它一次測試過多。該測試驗證三個依賴關係是否正確調用。如果這些依賴關係中的任何一個發生變化,這個測試將不得不改變。最好有3個獨立的測試,驗證每個依賴關係是否得到妥善處理。傳入一個存根對象,用於未測試的依賴關係(而不是模擬對象,因爲它會失敗)。

  2. Theres沒有驗證傳遞給依賴項的參數,所以這些測試是不完整的。

在本示例中,我將使用Moq作爲嘲諷庫。這個測試沒有指定所有依賴的行爲,它只測試一個調用。它還將檢查傳入的參數是否是給定輸入的期望值,輸入的變化將證明單獨的測試是合理的。

public void AddEventSavesSnapshot(object eventSnaphot) 
{ 
    Mock<IDocumentDao> mockDocumentDao = new Mock<IDocumentDao>(); 
    Mock<IPatientSnapshotService> mockPatientSnapshot = new Mock<IPatientSnapshotService>(); 

    string eventSample = Some.String(); 
    EventSection eventSection = new EventSection(eventSample); 

    mockPatientSnapshot.Setup(r => r.SaveEventSnapshot(eventSample)); 

    AddDocumentEventService sut = new AddDocumentEventService(); 
    sut.DocumentDao = mockDocumentDao; 
    sut.PatientSnapshotService = mockPatientSnapshot; 

    sut.AddEvent(eventSection); 

    mockPatientSnapshot.Verify(); 
} 

請注意,如果AddEvent()可能使用它們,那麼僅在此測試中才需要傳入未使用的依賴項。相當合理地,該類可能具有與此測試未涉及的相關性。

+0

感謝您的反饋。你能否詳細說明你提出的兩大要點。如果你有時間,也許會用一些僞代碼。 – 2010-01-05 19:10:54