Moq 的使用
Info
本文基于 Moq 官方快速入门:Quickstart · devlooped/moq Wiki · GitHub
本篇代码均基于如下接口和类型:
public interface IFoo
{Bar Bar { get; set; }string Name { get; set; }int Value { get; set; }bool DoSomething(string value);bool DoSomething(int number, string value);Task<bool> DoSomethingAsync();string DoSomethingStringy(string value);bool TryParse(string value, out string outputValue);bool Submit(ref Bar bar);int GetCount();bool Add(int value);event EventHandler FooEvent;
}
public class Bar
{public virtual Baz Baz { get; set; }public virtual bool Submit() { return false; }
}public class Baz
{public virtual string Name { get; set; }
}
1.1 Moq 中设置可覆写方法行为的方式
1.1.1 设置“可覆写方法”行为的主要方法
Moq 中设置方法行为的主要方法有三:
-
Setup()
方法:用于设置“可覆写方法(如接口方法)”的行为 -
Returns()
方法:作用于 Setup()
方法的返回值(ISetup
类型),用于设置被覆写方法的返回值 -
Throws()
方法:作用于 Setup()
方法的返回值(ISetup
类型),用于设置覆写方法发生错误时抛出的异常
下面通过不同场景介绍它们的用法。
Info
下文“设置方法的XXX”中的“方法”均指代“可覆写方法”
1.1.2 设置方法的返回值
设置“可覆写方法”的返回值通过 Setup()
方法和 Returns()
方法完成。
以如下代码为例,它为 DoSomething()
方法设置了参数为“ping”时对应的返回值。第 3 行代码将输出 false,第 4 行将输出 true
mock.Setup(foo => foo.DoSomething("ping")).Returns(true);mock.Object.DoSomething("test").Dump();
mock.Object.DoSomething("ping").Dump();
通过对 SetupSequence()
方法的返回值(类型为 ISetupSequentialResult
)连续调用 Returns()
方法,可以阶梯配置返回值。以如下代码为例,调用 mock.Object.GetCount()
时,将依次输出 3、2、1、0、抛出异常:
var mock = new Mock<IFoo>();
mock.SetupSequence(f => f.GetCount()).Returns(3).Returns(2).Returns(1).Returns(0).Throws(new InvalidOperationException());
1.1.3 设置异步方法的返回值
Setup()
、Returns()
方法搭配 .Result
属性可以为异步方法设置值:
mock.Setup(foo => foo.DoSomethingAsync().Result).Returns(true);
// 返回 true
(await mock.Object.DoSomethingAsync()).Dump();
1.1.4 自定义设置方法的返回值逻辑
通过向 Returns()
方法传入委托,可以自定义被设置方法的返回值。以如下代码为例,lambda 表达式设置了 DoSomething()
方法返回值的逻辑:
mock.Setup(x => x.DoSomethingStringy(It.IsAny<string>())).Returns ((string s) => s.ToLower());
// 输出 abcdef
mock.Object.DoSomethingStringy("ABCdef").Dump();
需要注意,受到闭包影响,我们可以借助闭包延迟赋值:
var count = 1;
mock.Setup(foo => foo.GetCount()).Returns (() => count);
count = 2;
// 因 count 被修改,如下代码返回 2
mock.Object.GetCount().Dump();
Info
关于
It.IsAny<string>()
,见1.2 Moq 的参数匹配
1.1.5 设置方法抛出异常
通过 Setup()
、Throws()
方法可以设置方法抛出异常的行为。以如下代码为例,它为 DoSomething()
方法设置了参数为“reset”、“string.Empty”时对应的行为。第 4、5 行将分别抛出 InvalidOperationException
和 ArgumentException
异常:
mock.Setup(foo => foo.DoSomething("reset")).Throws<InvalidOperationException>();
mock.Setup(foo => foo.DoSomething(string.Empty)).Throws(new ArgumentException("command"));mock.Object.DoSomething("reset").Dump();
mock.Object.DoSomething(string.Empty).Dump();
1.1.6 设置方法的引用参数(out、ref)
Moq 还可以设置引用实例对应的返回值(不会修改引用实例的实际值)。
以如下代码为例,我们令第一个参数为“ping”时,out 参数值为“ack”。如下代码 outValue 对应的值分别为“ack”、null:
var outString = "ack";
mock.Setup(foo => foo.TryParse("ping", out outString)).Returns(true);
string outValue;
mock.Object.TryParse("ping", out outValue).Dump();
outValue.Dump();
outValue = null;
mock.Object.TryParse("test", out outValue).Dump();
outValue.Dump();
如下代码演示了 ref 参数的使用,我们令传入的实参为 bar1
时返回 true,因此如下代码分别输出:true、false:
var bar1 = new Bar();
var bar2 = new Bar();
mock.Setup(foo => foo.Submit(ref bar1)).Returns(true);
mock.Object.Submit(ref bar1).Dump();
mock.Object.Submit(ref bar2).Dump();
1.1.7 设置 protected 方法
Moq 通过 Moq.Protected
命名空间下的 Protected()
扩展方法为“设定 protected 方法的行为”提供了方案。遗憾的是外部代码是无法访问受保护方法的,因此传递参数时需要直接输入字符串,且 IntelliSense 不会进行提示。
以如下代码为例,Moq 修改了两个 ExecuteCore()
方法的行为:
using Moq.Protected;var mock = new Mock<CommandBase>{ CallBase = true };
mock.Protected().Setup<int>("ExecuteCore").Returns(5);
mock.Protected().Setup<bool>("ExecuteCore", ItExpr.IsAny<string>()).Returns(true);mock.Object.Execute().Dump();
mock.Object.Execute("test").Dump();public class CommandBase
{public int Execute() => ExecuteCore();public bool Execute(string arg) => ExecuteCore(arg);protected virtual int ExecuteCore() => 1;protected virtual bool ExecuteCore(string arg) => false;
}
Notice
为了匹配参数类型,
Setup()
方法的第二个参数(params object[] args
)必须使用ItExpr
而非It
,否则会抛出异常!
借助媒介接口设置 protected 方法
在前面我们提到:“……外部代码是无法访问受保护方法的,因此传递参数时需要直接输入字符串,且 IntelliSense 不会进行提示”。为了解决这种不便,Moq 对 protected 添加了额外的支持:通过 As()
方法传入媒介接口,完成 protected 方法的预期设置。
以如下代码为例,CommandBase
并未实现 CommandBaseProtectedMembers
接口,但它们的 ExecuteCore()
方法的签名一致,通过 As()
方法将媒介接口引入,完成对 protected 方法的设置:
var mock = new Mock<CommandBase>{ CallBase = true };
mock.Protected().As<CommandBaseProtectedMembers>().Setup(m => m.ExecuteCore(It.IsAny<string>())).Returns(true);mock.Object.Execute("test").Dump();
interface CommandBaseProtectedMembers
{bool ExecuteCore(string arg);
}
1.1.8 设置 internal 方法
internal 方法的模拟需要借助友元程序集实现。用法如下:
// 强类型程序集需要键入 key
[assembly:InternalsVisibleTo("DynamicProxyGenAssembly2,PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]// 非强类型程序集,可以忽略 key
[assembly:InternalsVisibleTo("DynamicProxyGenAssembly2")]
其中,程序集的名称需要根据测试用例的程序集名称键入,上述代码仅是举例。
1.2 Moq 的参数匹配
我们可以通过 It
中的若干方法设置不同参数对应的返回值。
1.2.1 It.IsAny<T>()
匹配任意参数
-
It.IsAny<T>()
方法:用于匹配任意参数。
以如下代码为例,均输出 true:
mock.Setup(foo => foo.DoSomething(It.IsAny<string>())).Returns(true);
mock.Object.DoSomething(string.Empty).Dump();
mock.Object.DoSomething(null!).Dump();
mock.Object.DoSomething("test").Dump();
1.2.2 It.Ref.IsAny<T>()
匹配任意 ref 参数
-
It.Ref.IsAny()
方法:用于匹配任意 ref 参数。
以如下代码为例,均输出 true:
mock.Setup(foo => foo.Submit(ref It.Ref<Bar>.IsAny)).Returns(true);
Bar bar1 = new Bar();
Bar bar2 = new Bar();
mock.Object.Submit(ref bar1).Dump();
mock.Object.Submit(ref bar2).Dump();
1.2.3 It.Is<T>()
传入自定义规则匹配参数
-
It.Is<T>()
方法:用于匹配符合自定义规则的参数。
以如下代码为例,仅在输入参数为偶数时,返回 true。如下代码分别输出:false、true
mock.Setup(foo => foo.Add(It.Is<int>(i => i % 2 == 0))).Returns(true);
mock.Object.Add(1).Dump();
mock.Object.Add(2).Dump();
1.2.4 It.IsInRange<T>()
匹配范围内的参数
-
It.IsInRange<T>()
方法:用于匹配符合自定义规则的参数。入参需要实现 IComparable
接口。
以如下代码为例,仅在输入参数在 [0, 1] 之间时,返回 true。如下代码将输出 true、true、true、false:
mock.Setup(foo => foo.Add(It.IsInRange<int>(0, 10, Moq.Range.Inclusive))).Returns(true);
mock.Object.Add(0).Dump();
mock.Object.Add(1).Dump();
mock.Object.Add(10).Dump();
mock.Object.Add(11).Dump();
1.2.5 It.IsRegex()
匹配符合正则表达式的参数
-
It.IsRegex()
方法:用于匹配符合正则表达式的参数。
以如下代码为例,将输出 result1、result2、result2:
mock.Setup(x => x.DoSomethingStringy(It.IsRegex("[a-d]+", RegexOptions.IgnoreCase))).Returns("result1");
mock.Setup(x => x.DoSomethingStringy(It.IsRegex("[e-g]+", RegexOptions.IgnoreCase))).Returns("result2");
mock.Object.DoSomethingStringy("abcd").Dump();
mock.Object.DoSomethingStringy("Defg").Dump();
mock.Object.DoSomethingStringy("efg").Dump();
从这里可以看出,当两个规则都匹配时,以后一个添加的规则为准。
1.2.6 自定义参数规则
Moq 通过 Match.Create()
方法支持开发者自定义规则。它的用法如下:
var mock = new Mock<IFoo>();
mock.Setup(foo => foo.DoSomething(Match.Create<string>(s => !String.IsNullOrEmpty(s) && s.Length > 100))).Throws<ArgumentException>();
1.2.7 泛型参数的匹配
假设我们有一个泛型接口(如下),我们希望任何类型的参数都能正确传入。为此我们可以借助“多态”的行为,令泛型类型为 object
类型,此时该泛型方法将可以接受任何类型的参数。
public interface IFoo
{bool M1<T>();bool M2<T>(T arg);
}var mock = new Mock<IFoo>();
对应的代码如下:
mock.Setup(m => m.M1<object>()).Returns(true);
自 Moq 4.13 版本起,Moq 提供了 IsAnyType
、IsSubtype
类型,用于设置泛型参数类型的行为。
以如下代码为例,它设置泛型类型为任意类型时,输出 true,泛型类型为 IComparable
时,输出 false:
var mock = new Mock<IFoo>();mock.Setup(m => m.M1<It.IsAnyType>()).Returns(true);
mock.Setup(m => m.M1<It.IsSubtype<IComparable>>()).Returns(false);mock.Object.M1<string>().Dump();
mock.Object.M1<StringBuilder>().Dump();
进一步,配合 It.IsAny()
方法,可以限制泛型方法的入参类型:
mock.Setup(m => m.M2(It.IsAny<It.IsAnyType>())).Returns(true);
mock.Setup(m => m.M2(It.IsAny<It.IsSubtype<IComparable>>())).Returns(false);mock.Object.M2<string>("test").Dump();
mock.Object.M2<StringBuilder>(new StringBuilder()).Dump();
1.3 Moq 中设置属性的方式
1.3.1 属性 getter 返回值的设置
属性 getter 返回值的设置与设置方法的返回值相同,使用 Setup()
方法和 Returns()
方法完成。
如下代码将 mock.Object.Name
属性的 getter 返回值设为 bar:
mock.Setup(foo => foo.Name).Returns("bar");
mock.Object.Name.Dump();
此外 Moq 支持自动模拟层级结构(又名递归模拟),引用实例会相应的完成设置。
如下代码 mock.Object.Bar.Baz.Name
属性的 getter 返回值设置为 baz,且访问 getter 时没有发生空引用异常:
mock.Setup(foo => foo.Bar.Baz.Name).Returns("baz");
mock.Object.Bar.Baz.Name.Dump();
1.3.2 属性 setter 被调用时的行为设置
SetupSet()
方法用于设置“属性 setter 被调用时”的行为(需要搭配 Callback()
、Throws()
等方法),但它实际上并不会修改属性的值。
以如下代码为例,它输出了“callback”并抛出异常。而 mock.Object.Name
的值仍为 null。
mock.SetupSet(foo => foo.Name = "foo1").Callback(() => Console.WriteLine("callback"));
mock.SetupSet(foo => foo.Name = "foo2").Throws<InvalidOperationException>();mock.Object.Name = "foo1";
try
{mock.Object.Name = "foo2";
}
catch (Exception ex)
{Console.WriteLine(ex.Message);
}
mock.Object.Name.Dump();
1.3.3 验证属性的 setter 是否正确赋值
如上一节所述,“SetupSet()
方法实际上并不会修改属性的值”,即使 SetupSet()
会修改属性值,getter 返回的数据也可能是加工过的,我们无法通过断言等方式进行验证。
为此 Moq 提供了 VerifySet()
方法,用于检查属性的 setter 是否进行过赋值,赋值的内容是否匹配。
以如下代码为例,第一次验证时,因 mock.Object.Name
的 setter 未正确赋值,因此抛出异常;第二次验证时,该 setter 已被赋值过预期值,因此校验通过:
try
{mock.VerifySet(foo => foo.Name = "foo4");
}
catch (Exception ex)
{Console.WriteLine(ex.Message);
}
mock.Object.Name = "foo4";
mock.Object.Name = "foo5";
mock.VerifySet(foo => foo.Name = "foo4");
Info
更多内容,见 1.7.2 属性的验证
1.3.4 令属性 getter 返回 setter 接收的值
Moq 提供了 SetupProperty()
方法用于令属性的 getter 返回 setter 接收的值。它还可以设置属性的默认值(初始值)。用法如下:
// 设置 Name 属性的 getter 返回 setter 所接收的值
mock.SetupProperty(f => f.Name);
mock.Object.Name = "test";
// 输出 test
mock.Object.Name.Dump();// 设置默认值
mock.SetupProperty(f => f.Name, "foo");
// 输出 foo
mock.Object.Name.Dump();
Warn
SetupProperty()
会覆盖Setup()
方法设置的 getter 行为。以如下代码为例,它将输出 “test”而非“name1”:mock.Setup(f => f.Name).Returns(() => "name1"); mock.SetupProperty(f => f.Name); mock.Object.Name = "test"; mock.Object.Name.Dump();
1.3.5 令全体属性 getter 返回 setter 接收的值
通过 SetupProperty()
方法依次设置属性的 getter 行为显然很繁琐。为此 Moq 提供了 mock.SetupAllProperties()
方法,可以令全部属性的 getter 返回 setter 接收的值。用法如下:
// 调用 SetupAllProperties() 方法后,Value 的 getter 将返回 setter 设置的值
mock.SetupAllProperties();
mock.Object.Value = 2;
mock.Object.Name = "test";
// 输出 2、test
mock.Object.Value.Dump();
mock.Object.Name.Dump();
1.4 Moq 中事件的触发
本节代码均基于如下接口、类型和方法:
void DoSomething(object? sender, EventArgs e)
{(sender == mock.Object).Dump();e.Dump();
}public class FooEventArgs : EventArgs
{public FooEventArgs(string fooValue){ }
}
public interface IFoo
{event EventHandler FooEvent;IFoo2 Child { get; set; }void Submit();event EventHandler Sent;
}public interface IFoo2
{IFoo3 First { get; set; }
}public interface IFoo3
{event EventHandler FooEvent;
}
1.4.1 设置“事件订阅/取消订阅”的规则
Moq 通过 SetupAdd()
、SetupRemove()
方法设置事件订阅、取消订阅时的行为。用法如下:
mock.SetupAdd(m => m.FooEvent += It.IsAny<EventHandler>());
mock.SetupRemove(m => m.FooEvent -= It.IsAny<EventHandler>());
其中 It.IsAny<EventHandler>
表示 FooEvent
事件接收任何签名为 EventHandler
的事件处理器。
类似于 1.3.1 属性 getter 返回值的设置 中的模拟层级结构(递归模拟),事件的订阅/取消订阅也支持递归模拟:
mock.SetupAdd(m => m.Child.First.FooEvent += It.IsAny<EventHandler>());
mock.SetupRemove(m => m.Child.First.FooEvent -= It.IsAny<EventHandler>());
mock.Object.Child.First.FooEvent += DoSomething;
// 对于层级结构,moq 也可能正确模拟
mock.Raise(m => m.Child.First.FooEvent += null, new FooEventArgs(fooValue));
1.4.2 设置“事件订阅/取消订阅”时的行为
Moq 通过 Callback()
、Throws()
方法,订阅、取消订阅时可以进行触发回调、抛出异常等操作:
mock.SetupAdd(m => m.FooEvent += It.IsAny<EventHandler>()).Callback (() => Console.WriteLine("发生了订阅。"));
mock.SetupRemove(m => m.FooEvent -= It.IsAny<EventHandler>()).Throws<InvalidOperationException>();mock.Object.FooEvent += DoSomething;
mock.Object.FooEvent -= DoSomething;
1.4.3 触发事件
Moq 通过 Raise()
方法触发事件,用法如下:
string fooValue = "test";
// mock.Object 作为 sender。
mock.Raise(m => m.FooEvent += null, new FooEventArgs(fooValue));
// 当前实例作为 sender
mock.Raise(m => m.FooEvent += null, this, new FooEventArgs(fooValue));
Eureka
Raise()
方法中我们通过“m => m.FooEvent += null
”传入要触发的事件,此处 null 仅是占位符,并无实际用途。Moq 借助Action
对应的表达式树匹配对应的EventHandler
,null 的存在只是为了让 lambda 表达式成立。
Moq 也可以在方法/属性被调用后自动触发事件,这一点通过在 Setup()
方法后调用 Raises()
方法实现:
mock.Setup(foo => foo.Submit()).Raises(f => f.FooEvent += null, EventArgs.Empty);
mock.Object.Submit();
1.4.4 自定义签名事件的使用
Moq 也支持自定义事件,它的 Raise()
方法支持参数类型、数量任意的委托(通过 params object[]
数组实现)。下面是一个自定义签名事件的用例:
mock.SetupAdd(foo => foo.MyEvent += It.IsAny<MyEventHandler>());
mock.SetupRemove(foo => foo.MyEvent -= It.IsAny<MyEventHandler>());
mock.Object.MyEvent += DoSomething;
mock.Raise(foo => foo.MyEvent += null, 25, true);void DoSomething(int i, bool b)
{Console.WriteLine($"{i}-{b}");
}public delegate void MyEventHandler(int i, bool b);
public interface IFoo
{event MyEventHandler MyEvent;
}
1.5 Moq 中 Callback()
方法的使用
1.5.1 Callback()
的常规使用
Callback()
最常见的用法是传入 lambda 表达式,在指定方法(通过 Setup()
方法设定)调用时触发。下面是一个简单的用例:
var calls = 0;
mock.Setup(foo => foo.DoSomething("ping")).Callback(() => calls++).Returns(true);
mock.Object.DoSomething("test");
// 未传入指定数据,calls 未发生自增
calls.Dump();
// 在调用方法传入“ping”时,calls 发生自增
mock.Object.DoSomething("ping");
calls.Dump();
1.5.2 在方法返回前/返回后触发回调
Moq 还支持设置回调是在方法返回前还是返回后触发。当 Callback()
是在 Returns()
方法后调用,则是在方法返回后触发。
下面是一个简单的用例:
mock.Setup(foo => foo.DoSomething("ping")).Callback(() => Console.WriteLine("Before returns")).Returns(true).Callback(() => Console.WriteLine("After returns"));
// 调用 DoSomething() 后将依次输出“Before returns”、“After returns”
mock.Object.DoSomething("ping");
1.5.3 Callback()
访问方法的入参
Callback()
还可以访问方法的入参。入参值通过 lambda 的输入传递。
下面是一个简单的用例:
var callArgs = new List<string>();
mock.Setup(foo => foo.DoSomething(It.IsAny<string>())).Callback((string s) => callArgs.Add(s)).Returns(true);
mock.Object.DoSomething("test2");
callArgs.Dump();
Callback()
还有对应的泛型方法,上述代码可以改为如下形式:
mock.Setup(foo => foo.DoSomething(It.IsAny<string>())).Callback<string>(s => callArgs.Add(s)).Returns(true);
Callback()
也能访问多个参数:
mock.Setup(foo => foo.DoSomething(It.IsAny<int>(), It.IsAny<string>())).Callback<int, string>((i, s) => callArgs.Add(s)).Returns(true);
1.5.4 Callback()
访问引用参数
Callback()
也支持访问引用参数(ref、out 参数)。不过此类访问需要做一些额外的工作:需要自定义一个 ref/out 委托作为 Callback()
的入参。
下面是一个简单的用例:
mock.Setup(foo => foo.Submit(ref It.Ref<Bar>.IsAny)).Callback(new SubmitCallback((ref Bar bar) => Console.WriteLine($"Submitting a Bar! Baz.Name: {bar.Baz.Name}")));
var bar = new Bar{Baz = new Baz{Name = "Baz"}
};
// 输出“Submitting a Bar! Baz.Name: Baz”
mock.Object.Submit(ref bar);
delegate void SubmitCallback(ref Bar bar);
1.5.5 借助 Callback()
模拟方法内部行为
通过 Callback()
还可以模拟方法的内部行为。以如下代码为例,当 DoSomething()
方法被调用时,IFoo.Name
属性的值会被设置为 DoSomething()
的入参:
mock.SetupProperty(foo => foo.Name);
mock.Setup(foo => foo.DoSomething(It.IsAny<string>())).Callback((string s) => mock.Object.Name = s).Returns(true);mock.Object.DoSomething("test");
mock.Object.Name.Dump();
1.6 配置 Moq
Moq 的行为可以进行配置,下面是常用的配置:
1.6.1 配置校验严格程度
Moq 通过 MockBehavior
配置校验的严格程度。
MockBehavior
为枚举类型,成员有三:Strict
、Loose
和 Default
。其中 Default
和 Loose
行为一致。Strict
和 Loose
区别如下:
特性 | Strict Mock | Loose Mock |
---|---|---|
未定义调用处理 | 抛出 MockException | 返回默认值 |
测试控制粒度 | 必须完全定义 Expectation | 允许部分定义 |
测试稳定性 | 更严格(易失败) | 更宽容(易遗漏) |
适用测试阶段 | 完善测试阶段 | 开发验证阶段 |
以如下两段代码为例,第二段代码因未设置 IFoo.Name
的 getter,对 Name
属性的访问会抛出异常,而第一段代码对 IFoo.Name
的访问会得到 null:
var mock = new Mock<IFoo>();
mock.Object.Name.Dump();
var mock = new Mock<IFoo>(MockBehavior.Strict);
mock.Object.Name.Dump();
1.6.2 调用基类的默认实现
Moq 通过 CallBase
属性设置“未设置方法的期望时,是否调用基类的默认实现”。
以如下两段代码为例,第一段代码不输出任何内容,第二段代码则输出“DemoBase called”:
var mockBase = new Mock<DemoBase>();
mockBase.Object.DoSomething();public abstract class DemoBase
{public virtual void DoSomething(){Console.WriteLine($"{nameof(DemoBase)} called");}
}
var mockBase = new Mock<DemoBase>{CallBase = true
};
mockBase.Object.DoSomething();public abstract class DemoBase
{public virtual void DoSomething(){Console.WriteLine($"{nameof(DemoBase)} called");}
}
1.6.3. 配置模拟对象的默认值
Moq 可以通过 DefaultValue
属性设置“是否为内部成员创建模拟对象”。该属性是 DefaultValue
枚举类型,主要成员有:
-
Empty
:未显式配置的成员,返回类型对应的默认值 -
Mock
:未显式配置的成员,自动生成一个模拟对象作为返回值 -
Custom
:允许开发者继承DefaultValueProvider
类以自定义默认值生成逻辑。
以如下代码为例,可以通过 Mock.Get()
方法获取 Bar
实例对应的 Mock
对象,且通过 mock
、barMock
、value
调用 Submit()
方法都能得到预期值:
var mock = new Mock<IFoo>() { DefaultValue = DefaultValue.Mock };
Bar value = mock.Object.Bar;var barMock = Mock.Get(value);
barMock.Setup(b => b.Submit()).Returns(true);
value.Submit().Dump();
barMock.Object.Submit().Dump();
mock.Object.Bar.Submit().Dump();
DefaultValue.Custom
项的使用
前面我们提到:“Custom
:允许开发者继承 DefaultValueProvider
类以自定义默认值生成逻辑”。
使用 DefaultValueProvider
有一点不便:开发者需要为所有用到的类型编写默认值。为此 Moq 提供了 LookupOrFallbackDefaultValueProvider
抽象类型,它派生自 DefaultValueProvider
类型,并为所有类型提供了默认值,开发者只需编写自己需要的默认值。
下面是一个简单用例,其中 name
变量的值为“?”:
var mock = new Mock<IFoo> { DefaultValueProvider = new MyEmptyDefaultValueProvider() };
var name = mock.Object.Name;class MyEmptyDefaultValueProvider : LookupOrFallbackDefaultValueProvider
{public MyEmptyDefaultValueProvider(){base.Register(typeof(string), (type, mock) => "?");base.Register(typeof(List<>), (type, mock) => Activator.CreateInstance(type));}
}
1.7 Moq 的调用验证
1.7.1 方法的验证
1.7.1.1 验证方法调用
Moq 通过 Verify()
方法验证指定方法是否被调用过,不符合验证条件时将抛出异常。以如下代码为例,该验证要求 IFoo.DoSomething("ping")
方法发生过调用:
mock.Verify(foo => foo.DoSomething("ping"));
我们还可以通过 Verify()
传入参数,以自定义异常信息:
mock.Verify(foo => foo.DoSomething("ping"), "每次执行 x 操作,DoSomething(\"ping\") 方法都应该发生过调用。");
MockException | ||
Expected invocation on the mock at least once, but was never performed: foo => foo.DoSomething("ping") Performed invocations: Mock<IFoo:1> ... | ||
Message | Expected invocation on the mock at least once, but was never performed: foo => foo.DoSomething("ping") Performed invocations: Mock<IFoo:1> (foo): No invocations performed. | |
---|---|---|
InnerException | null | |
StackTrace | at Moq.Mock.Verify(Mock mock, LambdaExpression expression, Times times, String failMessage) in /_/src/Moq/Mock.cs:line 332
at Moq.Mockundefined expression) in /_/src/Moq/Mock`1.cs:line 810
at | |
Data |
| |
HelpLink | null | |
HResult | -2146233088 | |
IsVerificationError | True | |
Source | Moq | |
TargetSite |
Eureka
Moq 在验证方法调用时,通过两步判断传入的参数是否为期望参数(针对引用类型):
判断传入的实例的引用是否相同;
不相同则执行第二步。
通过实例的
Equals()
方法判断是否相等。使用的是参数的
Equals()
方法判断相应的方法是否进行了调用。以如下两段代码为例,第一段代码即使Demo.Equals()
始终返回 false,Verify()
仍认为被验证方法进行了正确的调用:var mock = new Mock<IFoo>(); Demo demo = new Demo(); mock.Object.DoSomething(demo); mock.Verify(m => m.DoSomething(demo));public class Demo {public override bool Equals(object? obj){return false;} }
第二段代码,两次传递的
Demo
实例并非同一个,但Demo.Equals()
始终返回 true,Verify()
会认为被验证方法进行了正确的调用:var mock = new Mock<IFoo>(); mock.Object.DoSomething(new Demo()); mock.Verify(m => m.DoSomething(new Demo()));public class Demo {public override bool Equals(object? obj){return true;} }
上述代码中用到的
IFoo
接口如下:public interface IFoo {public void DoSomething(object arg); }
此外,上述逻辑也适用于
Mock.SetUp()
设置方法行为时对应的实参。
1.7.1.2 验证方法调用次数
Verify()
方法还支持验证方法的调用次数。次数通过传入 Moq.Times
参数设定。
以如下代码为例,分别限制 DoSomehting()
方法调用 0 次、 至少 1 次、 至少 2 次:
mock.Object.DoSomething("test");
// 如下方法调用后,Verify() 会报错
mock.Object.DoSomething("ping1");
mock.Verify(foo => foo.DoSomething("ping1"), Times.Never);mock.Verify(foo => foo.DoSomething("ping2"), Times.AtLeastOnce());mock.Verify(foo => foo.DoSomething("ping3"), Times.AtLeast(2));
1.7.1.3 验证参数相匹配的方法的调用
在 1.7.1.1 验证方法调用中,我们验证了“指定参数的方法调用”。我们还可以通过 1.2 Moq 的参数匹配中提到的方式,验证规则相匹配的参数方法是否被调用。
以如下代码为例,它验证了 DoSomething()
方法调用时, 第一个参数是否大于 50 :
var mock = new Mock<IFoo>();
mock.Object.DoSomething(51, string.Empty);
mock.Verify(m => m.DoSomething(It.Is<int>(arg1 => arg1 > 50), It.IsAny<string>()));
1.7.2 属性的验证
1.7.2.1 验证属性 getter、setter 的调用
Moq 通过 VerifyGet()
、 VerifySet()
方法验证属性的 getter、setter 是否被调用。
下面是一个简单的示例,它们只验证 getter、setter 是否 被调用 ,不验证 属性值 :
mock.VerifyGet(foo => foo.Name);
mock.VerifySet(foo => foo.Name);
1.7.2.2 验证属性 setter 的赋值
VerifySet()
方法支持验证 setter 传入的值是否符合要求。以如下代码为例,它分别验证 IFoo.Name
是否被赋值为“ foo ”、IFoo.Value
所赋值是否 在 [1, 5] 之间 :
// 验证属性的 setter 是否被赋值为指定值
mock.Object.Name = "foo";
mock.VerifySet(foo => foo.Name = "foo");
// 验证属性的 setter 所赋值是否在指定范围内
mock.Object.Value = 1;
mock.VerifySet(foo => foo.Value = It.IsInRange(1, 5, Moq.Range.Inclusive));
1.7.3 事件的验证
Moq 通过 VerifyAdd()
、 VerifyRemove()
方法验证事件的 Add、Remove 是否进行过调用:
mock.Object.FooEvent += (sender, e) => Console.WriteLine("test");
mock.Object.FooEvent -= (sender, e) => Console.WriteLine("test");
// 验证事件的 Add、Remove 是否被调用过
mock.VerifyAdd(foo => foo.FooEvent += It.IsAny<EventHandler>());
mock.VerifyRemove(foo => foo.FooEvent -= It.IsAny<EventHandler>());
1.7.4 禁止其他方法调用
Moq 通过 VerifyNoOtherCalls()
方法限制只有设置过的方法可以调用,未设置的方法(包括不仅限于:方法、属性、事件)发生调用将 抛出异常 。它用于验证是否存在意外的调用。
下面是一个简单的用例,因 IFoo.GetCount()
方法未设置验证规则,因此 VerifyNoOtherCalls()
会抛出异常:
mock.Object.GetCount();
mock.VerifyNoOtherCalls();
1.7.5 调用顺序的验证
Moq 支持对调用顺序进行限制。该功能通过 MockSequence
类型、 Mock.InSequence()
方法搭配实现。
以如下代码为例,它要求 IService.GetToken()
先于 IService.GetData()
调用,否则会抛出异常:
var mock1 = new Mock<IService>(MockBehavior.Strict);
var sequence = new MockSequence();mock1.InSequence(sequence).Setup(x => x.GetToken()).Returns("123456789");
mock1.InSequence(sequence).Setup(x => x.GetData()).Returns("987654321");mock1.Object.GetToken();
mock1.Object.GetData();
调用顺序的验证不仅限于同一个 Mock 实例,可以多个 Mock 实例混合:
var serviceMock = new Mock<IService>(MockBehavior.Strict);
var clientMock = new Mock<IClient>(MockBehavior.Strict);
var sequence = new MockSequence();serviceMock.InSequence(sequence).Setup(x => x.GetToken()).Returns("123456789");
clientMock.InSequence(sequence).Setup(x => x.Check(It.IsAny<string>())).Returns(true);
serviceMock.InSequence(sequence).Setup(x => x.GetData()).Returns("987654321");string token = serviceMock.Object.GetToken();
clientMock.Object.Check(token);
serviceMock.Object.GetData();
上述代码用到的接口如下:
public interface IClient
{bool Check(string token);
}
public interface IService
{string GetToken();string GetData();
}
Warn
其中 Mock 的校验严格程度必须为
MockBehavior.Strict
。更多内容见 1.6.1 配置校验严格程度
1.7.6 集中式验证
Moq 提供了 MockRepository
类型,该类型是一个对象工厂 + 管理中心。通过它可以统一配置模拟对象的严格程度、调用基类的默认实现等参数。通过它还可以集中进行验证,该功能需要搭配 Verifiable()
方法共同使用。
下面是一个简单的示例,因 Bar.Submit()
方法被标记为“可证实(Verifiable)”,而它又未进行调用,为此 MockRepository.Verify()
方法 抛出了异常 :
var repository = new MockRepository(MockBehavior.Strict) { DefaultValue = DefaultValue.Mock };var fooMock = repository.Create<IFoo>();
var barMock = repository.Create<Bar>();fooMock.Setup(x => x.DoSomething("test")).Returns(true).Verifiable();
barMock.Setup(x => x.Submit()).Returns(false).Verifiable();fooMock.Object.DoSomething("test");
// barMock.Object.Submit();// 同时验证 IFoo 和 Bar
repository.Verify();
1.8 其他
1.8.1 重置模拟对象
Moq 提供了 Reset()
方法用于重置模拟对象的预期设置。用法如下:
mock.Reset();
1.8.2 获取模拟对象背后的 Mock 实例
Moq 提供的 Mock.Get()
方法可以传入模拟对象,它会返回其背后的 Mock 实例。用法如下:
IFoo foo = (new Mock<IFoo>()).Object;var fooMock = Mock.Get(foo);
fooMock.Setup(f => f.GetCount()).Returns(42);
foo.GetCount().Dump();
1.8.3 模拟多个接口
Moq 提供了 As()
方法用于模拟实现多个接口。用法如下:
var mock = new Mock<IFoo>();
// 通过 As() 方法可以令 Mock 模拟多个接口。
var disposableFoo = mock.As<IDisposable>();
disposableFoo.Setup(disposable => disposable.Dispose());
上述代码可以简化为一行:
mock.As<IDisposable>().Setup(disposable => disposable.Dispose());
Warn
需要注意的是,
As()
方法的调用要在访问 Mock.Object
属性前发生。以如下代码为例,它会 抛出异常 :var mock = new Mock<IFoo>(); IFoo foo = mock.Object; var disposableFoo = mock.As<IDisposable>();
1.8.4 Mock 的链式表达式(LINQ to Mocks)
类似于 LINQ to XML、LINQ to Json 可以一行代码完成查询,Moq 提供了 Mock.Of()
方法,开发者可以通过该方法以“链式”的方式设置模拟对象的行为。
以如下代码为例,我们通过链式的方式对 IList
接口进行了模拟:
var list = Mock.Of<IList>(x => x.IsReadOnly == true &&x.IsFixedSize == false &&x.Add(1) == 2 &&x.Add(It.IsInRange<int>(3, 10, Moq.Range.Inclusive)) == 6 &&x.Contains(It.Is<int>(i => i % 2 == 0)) == true &&x.IndexOf(7) == 8);// 当 Add() 方法插入数字 1,返回 2
// 当 Add() 方法插入 [3, 10] 之间的数字,返回 6
// Contains() 方法查询的数据为偶数时,返回 true
// IndexOf() 方法查询 7 的位置时,返回 8
list.IsReadOnly.Dump();
list.IsFixedSize.Dump();
list.Add(1).Dump();
list.Add(2).Dump();
list.Add(3).Dump();
list.Add(10).Dump();
list.Contains(11).Dump();
list.Contains(12).Dump();
list.IndexOf(7).Dump();
list.IndexOf(9).Dump();
如果你确实需要模拟对象背后的 Mock
实例,可以通过 Mock.Get()
方法获取(见1.8.2 获取模拟对象背后的 Mock 实例)