.net core 單元測試之 JustMock第二篇
- 2019 年 10 月 3 日
- 筆記
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
(Expression<Predicate > expression) - 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靜態類,需要用到付費版的高級功能。更好的時候,我們應該避免這些依賴,以方便我們測試。