今天會是基本介紹 NSubstitute 的最後一個篇章 XD (含今天花了四天的篇幅介紹,還有一些比較深的寫法看之後能否抽出時間補上),那今天還會介紹幾個也算是常見的語法 DidNotReceive、ReturnsForAnyArgs、AndDoes、When...Do、Callback builder 與 Throwing Exceptions,商業邏輯主角跟昨天一樣是官方網站提供的計算機,分別有模式 Mode 屬性與 Add 方法,程式碼如下:
namespace CalculatorLibrary
{
    public interface ICalculator
    {
        string Mode { get; set; }
        int Add(int a, int b);
    }
}
除此之外,今天會因應模擬物件繼承不會有回傳值的介面,提供另一個商業邏輯程式碼,如下:
public interface IFoo {
    void SayHello(string to);
}
在昨天我們一起認識了 Received 方法,來確認是否有執行,細一點可以確認執行幾次;那 DidNotReceive 就是 指驗證是否沒有接收,用 Received 的寫法就是 Received(0)(接收 0 次),範例如下:
[Test]
public void DemoDidNotReceiveTest()
{
    // Arrange
    var calculator = Substitute.For<ICalculator>();
    // Act
    calculator.Add(1, 2);
    calculator.Add(-100, 100);
    // Arrange
    calculator.DidNotReceive()
              .Add(Arg.Any<int>(), Arg.Is<int>(x => x >= 500));
}
很多時候在撰寫驗證商業邏輯的測試時,其實引用的第三方套件我們不太在意其中間流程,只在乎最終結果(如回傳值)是符合我們預期的就好;此時,ReturnsForAnyArgs 方法就可以幫助我們,這隻方法是輸入的方法不管是什麼,所回傳的數值必然是我們所設定的期望值,其範例如下:
[Test]
public void DemoReturnsForAnyArgsTest()
{
    // Arrange
    var calculator = Substitute.For<ICalculator>();
    calculator.Add(1, 2).ReturnsForAnyArgs(100);
    // calculator.Add(default, default).ReturnsForAnyArgs(100);
    // Act + Assert
    Assert.AreEqual(100, calculator.Add(1, 2));
}
PS:面對 ReturnsForAnyArgs 的重點是在最後的回傳值,其參數可使用 C# 提供的 default (預設值運算式) 做處理。
在談論 AndDoes 之前,必須先瞭解一個概念:CallBack,而 CallBack 簡單來說就是指一個程式執行完再去執行另一個程式 (參考來源:什麼是Callback函式),而 AndDoes 就是指要執行的下一隻程式碼,範例如下:
[Test]
public void DemoAndDoesTest()
{
    // Arrange
    var counter = 0;
    var calculator = Substitute.For<ICalculator>();
    calculator.Add(default, default)
              .ReturnsForAnyArgs(x => 0)
              .AndDoes(x => counter++);
    // Act
    calculator.Add(7, 3);
    calculator.Add(2, 2);
    // Assert
    Assert.AreEqual(counter, 2);
}
When..Do 總共需要設定兩個設定來啟動 CallBack 機制;第一步,去呼叫方法(大多時候是 void 方法,但其實可以用在有回傳值的方法,但不建議其理由是有回傳值的方法建議用 Returns() 方法,讓撰寫的測試法可以區別哪些是有回傳值,那些則不);其次,利用 Do() 方法啟動下一個方法,示範的程式碼如下:
[Test]
public void DemoWhenDoTest() {
    // Arrange
    var counter = 0;
    var foo = Substitute.For<IFoo>();
    
    foo.When(x => x.SayHello("World"))
       .Do(x => counter++);
    // Act
    foo.SayHello("World");
    foo.SayHello("World");
    
    // Arrange
    Assert.AreEqual(2, counter);
}
倘若我們要在 When..Do 方法裡面的 Do 方法撰寫一系列的 CallBack 方法們,則可以建置一個 CallBack Builder,建置完後再執行 CallBack Builder 的子方法(因篇幅關係,這邊就先不探討子方法的細節了),示範程式碼如下:
[Test]
public void DemoCallbackBuilderTest() {
    // Arrange
    var sub = Substitute.For<ISomething>();
    var calls = new List<string>();
    var counter = 0;
    
    sub.When(x => x.Something())
       .Do(
           Callback.First(x => calls.Add("1"))
                   .Then(x => calls.Add("2"))
                   .Then(x => calls.Add("3"))
                   .ThenKeepDoing(x => calls.Add("+"))
                   .AndAlways(x => counter++)
           );
    // Act
    for (int i = 0; i < 5; i++)
    {
        sub.Something();
    }
    
    // Arrange
    Assert.That(String.Concat(calls), Is.EqualTo("123++"));
}
那今天最後要提到的就是如何撰寫例外處理,搭配有回傳值與無回傳值的方法提供兩種寫法,示範的程式碼如下:
[Test]
public void DemoThrowingExceptionWithReturnsTest()
{
    // Arrange
    var calculator = Substitute.For<ICalculator>();
    calculator.Add(-1, -1).Returns(x => { throw new Exception(); });
    // Act + Assert
    Assert.Throws<Exception>(() => calculator.Add(-1, -1));
}
[Test]
public void DemoThrowingExceptionWithWhenDoTest()
{
    // Arrange
    var calculator = Substitute.For<ICalculator>();
    calculator.When(x => x.Add(-2, -2))
              .Do(x => { throw new Exception(); });
    // Act + Assert
    Assert.Throws<Exception>(() => calculator.Add(-2, -2));
}
終於算把常用的 NSubstitute 語法告一個段落了,這幾天介紹的語法其最終的目的就是要協助我們快速建置假物件,省下撰寫假物件的時間;那接下來就要討論單元測試必然要討論的議題:重構(Refactoring)與接縫(Seam)。
PS:又是一個很深的大坑![]()