今天會是基本介紹 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:又是一個很深的大坑