[91大的TDD課程心得] 三種驗證狀況與測試獨立性

一般測試會有三種需要驗證的狀況,最麻煩的物件互動該如何驗證?

該如何設計才能保持測試的獨立性,提高程式的可測性?

 

三種驗證狀況:

  • 驗證目標函式的回傳值
  • 驗證目標物件的狀態
  • 驗證目標物件是否正確地與其他物件互動

前兩點的實做相對明瞭,在測試框架 MSTest這可以涵蓋大多數的狀況,
那要如何驗證第三種情況呢?

可使用 NSubstitute 當中的 mock 物件:

log.Received(1).Save(Arg.Is(x => x.Contains("login failed")));
說明:log 這個物件的 Save function 應該要被呼叫1次,且傳入參數為一個 string,
且此 string必須含有 "login failed" 的字樣。

※ Mock 一般驗證的條件較細,會提高維護成本,斟酌使用


測試獨立

為什麼要獨立、要隔離?

  • 執行速度快
  • 關注點分離
  • 單一職責
  • 獨立測試(可測試性)
  • 測試程式的健壯性(Robustness)
    ※ 妥妥的,不容易壞啊~

沒獨立產生的問題:

  • 測試可能因為相依而變慢
  • 相依物件發生問題導致誤判測試的結果
  • 等相依物件開發完成,才能測試目標物件
  • 無法測試(無法模擬出該測試環境)

如何解決?

  • 單一職責
  • 依賴介面(interface)
  • 依賴注入(控制反轉、Dependency Injection)
    將依賴的物件型態給抽象化為 interface,降低物件之間的耦合度
    再由外部透過 constructor、property 等方式「注入」
  • 測試程式中自行定義 stub 物件
    依賴的物件抽象化之後,就可在測試程式中模擬出抽性型別的假資料注入
    以便進行「互動」部份的測試。

    可使用 NSubstitute 套件,大幅加速 stub 物件的撰寫!
    var p = Substitute.For<XXX>();
    p.GetPassword("joey").Returns("91");

    var t = Substitute.For<XXX>();
    t.GetRandom("").ReturnsForAnyArgs("000000")


    這部份偶有作假資料的感覺,但因為要測的部份其實「互動」的部份,
    假資料並不是測試重點,是相當合理的。

    根據個人經驗,Substitute.For<> 比較合適代入 Interface,而非 Class。
    官網有提到這部份的訊息:
    Warning: Substituting for classes can have some nasty side-effects. For starters, NSubstitute can only work with virtual members of the class, so any non-virtual code in the class will actually execute! If you try to substitute for your class that formats your hard drive in the constructor or in a non-virtual property setter then you’re asking for trouble. If possible, stick to substituting interfaces.
    http://nsubstitute.github.io/help/creating-a-substitute/
不是為了撰寫 Unit Test 而將程式碼改為 Dependency Injection,是本身程式碼寫的不夠好,
耦合度太高,又缺乏可測性,該重構啊~