.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静态类,需要用到付费版的高级功能。更好的时候,我们应该避免这些依赖,以方便我们测试。