.net core 單元測試之 JustMock第二篇
阿新 • • 發佈:2019-08-28
JustMock標記方法
上篇文章在舉例子的時候使用了returns的標記方法,JustMock還有很多標記方法:
- CallOriginal
跟Behaviors裡的CallOriginal差不多意思,被呼叫時執行原始的方法和屬性的實現。 - DoNothing
忽略對方法或者屬性的呼叫。 - DoInstead
替換原來方法的呼叫,或者屬性的設定。 - MustBeCalled
被標記的方法必須呼叫,否則會丟擲異常。 - Raise
當我們需要測試Event是否執行時,我們可以使用Raise來引發Events。
例子:
public delegate void CustomEvent(string value); public interface IFoo { event CustomEvent CustomEvent; }
//測試方法: [Test] public void ShouldInvokeMethodForACustomEventWhenRaised() { string expected = "ping"; string actual = string.Empty; var foo = Mock.Create<IFoo>(); foo.CustomEvent += delegate (string s) { actual = s; }; //引發事件,並傳遞引數 Mock.Raise(() => foo.CustomEvent += null, expected); Assert.AreEqual(expected, actual); }
- Raises
Raises用在呼叫方法後出發事件。
例子:
//使用的介面 public delegate void CustomEvent(string value, bool called); public delegate void EchoEvent(bool echoed); public delegate void ExecuteEvent(); public interface IFoo { event CustomEvent CustomEvent; event EchoEvent EchoEvent; event ExecuteEvent ExecuteEvent; void RaiseMethod(); string Echo(string arg); void Execute(); void Execute(string arg); }
當呼叫方法時觸發:
//測試方法
[Test]
public void ShouldRaiseCustomEventOnMethodCall()
{
string actual = string.Empty;
bool isCalled = false;
var foo = Mock.Create<IFoo>();
foo.CustomEvent += (s, c) => { actual = s; isCalled = c; };
Mock.Arrange(() => foo.RaiseMethod()).Raises(() => foo.CustomEvent += null, "ping", true);
foo.RaiseMethod();//呼叫方法,觸發事件
Assert.AreEqual("ping", actual);
Assert.IsTrue(isCalled);
}
另外還可以設定引數滿足條件時呼叫的方法觸發,設定延時觸發。
- Returns
設定返回結果。 - Throws
當一個方法呼叫時,丟擲異常。
例子:
[Test]
public void ShouldThrowExceptionOnMethodCall()
{
// Arrange
var foo = Mock.Create<IFoo>();
Mock.Arrange(() => foo.Execute(string.Empty)).Throws<ArgumentException>();
Assert.Throws<ArgumentException>(() => foo.Execute(string.Empty));
}
- ThrowsAsync
呼叫非同步方法時丟擲異常。(官方文件有這個標記,貌似使用21912071版本時沒有這個標記了) - When
當滿足條件時執行某操作。
例子:
public interface IFoo
{
bool IsCalled();
string Prop { get; set; }
}
[Test]
public void IsCalled_ShouldReturnTrue_WithMockedDependencies()
{
var foo = Mock.Create<IFoo>();
Mock.Arrange(() => foo.Prop).Returns("test");//設定Prop返回"Test"
Mock.Arrange(() => foo.IsCalled()).When(() => foo.Prop == "test").Returns(true);
Assert.IsTrue(foo.IsCalled());
}
JustMock模擬屬性
上面的例子中已經用到Returns來模擬屬性的值了,這裡再看看還有其它的用法:
- 根據索引模擬值
假設一個類的屬性是陣列或者連結串列等,我們需要模擬其中某個索引下的值:
//indexedFoo是一個數組
Mock.Arrange(() => indexedFoo[1]).Returns("pong"); //Mock indexedFoo 下標1的值為“pong”
- 設定屬性
我們需要驗證一個屬性是否被正確賦值時,可以用ArrangeSet
來模擬設定屬性並驗證。
[Test]
public void ShouldAssertPropertySet()
{
var foo = Mock.Create<IFoo>();
Mock.ArrangeSet(() => foo.Value = 1);
foo.Value = 1; //執行賦值
Mock.AssertSet(() => foo.Value = 1);
}
Matchers 匹配引數
我們模擬有引數的方法時,要根據不同的引數設定不同的返回值,Matchers可以做到這些。
- Arg.AnyBool,Arg.AnyDouble,Arg.AnyFloat, Arg.AnyGuid, Arg.AnyInt, Arg.AnyLong, Arg.AnyObject, Arg.AnyShort, Arg.AnyString, Arg.NullOrEmpty
- Arg.IsAny
- Arg.IsInRange([FromValue : int], [ToValue : int], [RangeKind])
- Arg.Matches
- Arg.Ref()
- Args.Ignore()
我們可以利用Arg.Matches<T>(Expression<Predicate<T>> expression)
生成我們想要的資料條件。
Arg.Ref() 是C#裡的ref 引數。
Args.Ignore() 可以忽略引數,同標記方法IgnoreArguments()
。
Matchers 不僅在構造引數中使用,還可以在Assert中使用:
[Test]
public void ShouldUseMatchersInAssert()
{
var paymentService = Mock.Create<IPaymentService>();
paymentService.ProcessPayment(DateTime.Now, 54.44M);
// 斷言:不管DateTime什麼時間,Payment amount都是54.44.
Mock.Assert(() => paymentService.ProcessPayment(
Arg.IsAny<DateTime>(),
Arg.Matches<decimal>(paymentAmount => paymentAmount == 54.44M)));
}
Sequential Mocking 連續模擬
Sequential mocking 允許我們多次執行程式碼返回不一樣的值,具體程式碼理解:
[TestMethod]
public void ShouldArrangeAndAssertInASequence()
{
var foo = Mock.Create<IFoo>();
//只需要在Arrange()後面加上InSequence()
Mock.Arrange(() => foo.GetIntValue()).Returns(0).InSequence();
Mock.Arrange(() => foo.GetIntValue()).Returns(1).InSequence();
Mock.Arrange(() => foo.GetIntValue()).Returns(2).InSequence();
int actualFirstCall = foo.GetIntValue(); //結果是:0
int actualSecondCall = foo.GetIntValue(); //結果是:1
int actualThirdCall = foo.GetIntValue(); //結果是:2
int actualFourthCall = foo.GetIntValue(); // 注意這是第四次呼叫,因為沒有設定結果,實際上他應當是上次呼叫的結果:2
}
配合Matcher使用:
Mock.Arrange(() => foo.Echo(Arg.Matches<int>(x => x > 10))).Returns(10).InSequence();
Mock.Arrange(() => foo.Echo(Arg.Matches<int>(x => x > 20))).Returns(20).InSequence();
int actualFirstCall = foo.Echo(11); //結果10
int actualSecondCall = foo.Echo(21); //結果20
還可以多次返回:
Mock.Arrange(() => foo.Echo(Arg.AnyInt)).Returns(10).Returns(11).MustBeCalled();
Assert.AreEqual(10, foo.Echo(1));
Assert.AreEqual(11, foo.Echo(2));
或者以陣列方式返回:
int[] returnValues = new int[3] { 1, 2, 3 };
Mock.Arrange(() => foo.Echo(Arg.AnyInt)).ReturnsMany(returnValues);
var actualFirstCall = foo.Echo(10); // 結果:1
var actualSecondCall = foo.Echo(10); // 結果:2
var actualThirdCall = foo.Echo(10); // 結果:3
Recursive Mocking 遞迴模擬
遞迴Mock。有時候我們需要取值像這樣的:foo.Bar.Baz.Do("x")
,我們可以不用一個一個物件去模擬,我們只需要Mock 第一個,然後設定值就可以了。
像這樣:
var foo = Mock.Create<IFoo>(); // mock 第一個物件
Mock.Arrange(() => foo.Bar.Do("x")).Returns("xit"); //設定某次呼叫的值
Mock.Arrange(() => foo.Bar.Baz.Do("y")).Returns("yit");
最後
這裡涉及到的都是是JustMock的免費版功能。在工作中遇到一些問題,我們需要測試的目標方法中混有靜態類提供的邏輯,免費的框架都不支援Mock靜態類,需要用到付費版的高階功能。更好的時候,我們應該避免這些依賴,以方便我們測試