2017-04-25 71 views
2

我編碼了大約12年,但我從來沒有習慣過TDD。此TDD嘗試的最佳實踐

好吧,事情即將改變,但由於我自己都在學習,所以我希望你們能幫助我。

我發佈了一個非常簡單的胸部類遊戲的例子。 當玩家抓住胸部時,它會記錄獲得的當前時間。 這個胸部需要一些時間才能打開,所以我需要出於UI的原因來顯示打開所需的剩餘時間。 每個胸部都有一個類型,並且這種類型綁定到一個數據庫值,該數據庫值將打開多少時間。

這是一個「沒有測試只是把事情做成快速心態」。考慮ChestsDatabase和DateManager是包含數據庫綁定值和當前系統時間包裝到類中的單例。

public class Chest { 
    private readonly int _type; 
    private readonly float _timeObtained; 

    public Chest(int type, float timeObtained) { 
     _type = type; 
     _timeObtained = timeObtained; 
    } 

    public bool IsOpened() { 
     return GetRemainingTime() <= 0; 
    } 

    // It depends heavily on this concrete Singleton class 
    public float GetRemainingTime() { 
     return ChestsDatabase.Instance.GetTimeToOpen(_type) - GetPassedTime(); 
    } 

    // It depends heavily on this concrete Singleton class 
    private float GetPassedTime() { 
     return DateManager.Instance.GetCurrentTime() - _timeObtained; 
    } 
} 

當然,我可以在一個依賴注入的方式取得,並擺脫單身:

public class Chest { 
    private readonly ChestsDatabase _chestsDatabase; 
    private readonly DateManager _dateManager; 
    private readonly int _type; 
    private readonly float _timeObtained; 

    public Chest(ChestsDatabase chestsDatabase, DateManager dateManager, int type, float timeObtained) { 
     _chestsDatabase = chestsDatabase; 
     _dateManager = dateManager; 
     _type = type; 
     _timeObtained = timeObtained; 
    } 

    public bool IsOpened() { 
     return GetRemainingTime() <= 0; 
    } 

    public float GetRemainingTime() { 
     return _chestsDatabase.GetTimeToOpen(_type) - GetPassedTime(); 
    } 

    private float GetPassedTime() { 
     return _dateManager.GetCurrentTime() - _timeObtained; 
    } 
} 

如果我使用接口來表達相同的邏輯是什麼?這將會更「TDD友好」,對吧? (假設我已經先完成了測試,當然)

public class Chest { 
    private readonly IChestsDatabase _chestsDatabase; 
    private readonly IDateManager _dateManager; 
    private readonly int _type; 
    private readonly float _timeObtained; 

    public Chest(IChestsDatabase chestsDatabase, IDateManager dateManager, int type, float timeObtained) { 
     _chestsDatabase = chestsDatabase; 
     _dateManager = dateManager; 
     _type = type; 
     _timeObtained = timeObtained; 
    } 

    public bool IsOpened() { 
     return GetRemainingTime() <= 0; 
    } 

    public float GetRemainingTime() { 
     return _chestsDatabase.GetTimeToOpen(_type) - GetPassedTime(); 
    } 

    private float GetPassedTime() { 
     return _dateManager.GetCurrentTime() - _timeObtained; 
    } 
} 

但是我到底該如何測試? 會是這樣嗎?

[Test] 
    public void SomeTimeHavePassedAndReturnsRightValue() 
    { 
     var mockDatabase = new MockChestDatabase(); 
     mockDatabase.ForType(0, 5); // if Type is 0, then takes 5 seconds to open 
     var mockManager = new MockDateManager(); 
     var chest = new Chest(mockDatabase, mockManager, 0, 6); // Got a type 0 chest at second 6 
     mockManager.SetCurrentTime(8); // Now it is second 8 
     Assert.AreEqual(3, chest.GetRemainingTime()); // Got the chest at second 6, now it is second 8, so it passed 2 seconds. We need 5 seconds to open this chest, so the remainingTime is 3 
    } 

這是合乎邏輯的嗎?我錯過了什麼嗎?因爲這看起來很大,如此錯綜複雜,所以......錯了。爲了進行這些測試,我必須創建兩個額外的課程。

讓我們帶着嘲弄的框架看:

[Test] 
    public void SomeTimeHavePassedAndReturnsRightValue() 
    { 
     var mockDatabase = Substitute.For<IChestsDatabase>(); 
     mockDatabase.GetTimeToOpen(0).Returns(5); 
     var mockManager = Substitute.For<IDateManager>(); 
     var chest = new Chest(mockDatabase, mockManager, 0, 6); 
     mockManager.GetCurrentTime().Returns(8); 
     Assert.AreEqual(3, chest.GetRemainingTime()); 
    } 

我得到的框架擺脫兩班的,不過,我覺得有什麼不對勁。在我的邏輯中有一種更簡單的方法嗎?在這種情況下,你會使用模擬框架還是實現類?

你們會完全擺脫這些測試嗎?還是堅持我的任何解決方案?或者如何使這個解決方案更好?

希望你能在我的TDD旅程中幫助我。謝謝。

回答

3

爲了您的目前的設計你的最後一次嘗試在邏輯上是正確的,並且接近我認爲的最佳測試案例。

我建議將模擬變量提取到字段。我還將重新排列測試線,以便在設置,執行和驗證之間有明確的區別。將胸部類型提取爲常量也會使測試更易於理解。

private IChestsDatabase mockDatabase = Substitute.For<IChestsDatabase>(); 
private IDateManager mockManager = Substitute.For<IDateManager>(); 
private const int DefaultChestType = 0; 

[Test] 
public void RemainingTimeIsTimeToOpenMinusTimeAlreadyPassed() 
{ 
    mockDatabase.GetTimeToOpen(DefaultChestType).Returns(5); 
    mockManager.GetCurrentTime().Returns(6+2); 
    var chest = new Chest(mockDatabase, mockManager, DefaultChestType, 6); 

    var remainingTime = chest.GetRemainingTime(); 

    Assert.AreEqual(5-2, remainingTime); 
} 

現在爲更一般的評論。 TDD的主要優勢在於它可以爲您提供有關您設計的反饋。測試代碼很大,錯綜複雜和錯誤的感覺是一個重要的反饋。把它想象成一個design pressure。測試將會改善測試重構和設計改進。

爲您的代碼,我會考慮這些設計問題:

  1. 均被分配適當的職責是什麼?特別是,它是否知道通過和剩餘時間胸部的責任?
  2. 設計中是否缺少任何概念?也許每個胸部都有一個鎖,並且有一個時基鎖。
  3. 如果我們在施工時通過了TimeToOpen而不是Type到胸部怎麼辦?把它看作是通過針頭,而不是通過乾草堆,其中針還沒有找到。僅供參考,請參閱this post

爲了測試如何能提供設計反饋商量好了,指的是由史蒂夫·弗里曼和NAT普賴斯測試的指導下成長的面向對象的軟件。

對於在C#中編寫可讀的測試的一套很好的做法,我推薦Roy Osherove的單元測試藝術。

2

但是也有一些需要而編寫單元測試,如圖單元測試

  1. 單獨的項目要考慮的一些要點。

  2. 一類用於編寫一類主要 代碼中函數的單元測試。

  3. 功能內涵蓋條件。
  4. 測試驅動開發(TDD)

如果你真的想知道更多(舉例),看看這個教程

單元測試C# - 最佳實踐https://www.youtube.com/watch?v=grf4L3AKSrs