Moq 的使用
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 实例)

浙公网安备 33010602011771号