当前位置: 首页 > news >正文

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() 时,将依次输出 3210抛出异常

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 行将分别抛出 InvalidOperationExceptionArgumentException 异常:

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,因此如下代码分别输出:truefalse

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。如下代码分别输出:falsetrue

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。如下代码将输出 truetruetruefalse

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() 方法:用于匹配符合正则表达式的参数。

以如下代码为例,将输出 result1result2result2

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 提供了 IsAnyTypeIsSubtype 类型,用于设置泛型参数类型的行为。

以如下代码为例,它设置泛型类型为任意类型时,输出 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 为枚举类型,成员有三:StrictLooseDefault。其中 DefaultLoose 行为一致。StrictLoose 区别如下:

特性 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 对象,且通过 mockbarMockvalue 调用 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.
InnerExceptionnull
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 
UserQuery.Main(), line 343
Data
(0 items)
HelpLinknull
HResult-2146233088
IsVerificationErrorTrue
SourceMoq
TargetSite
System.Reflection.MethodBase

Eureka

Moq 在验证方法调用时,通过两步判断传入的参数是否为期望参数(针对引用类型):

  1. 判断传入的实例的引用是否相同;

    不相同则执行第二步。

  2. 通过实例的 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 实例)

http://www.wuyegushi.com/news/815.html

相关文章:

  • InnoDB架构
  • 离线安装node.js node-red,及设置为服务注意事项
  • 北航操作系统上机实验使用vscode指南
  • Go 实现图像预处理 + OCR 的验证码识别流程
  • 7.27随笔
  • 实现图像预处理 + OCR 的验证码识别流程
  • 当 think 遇上 tool:深入解析 Agent 的规划之道
  • nonono
  • 2025.7.27学习日记
  • PG系列:PG数据库中分析操作系统IO是否正常
  • 【音频硬件相关】喇叭的阻值——了解阻抗:万用表测喇叭,测的是什么?
  • 【音频硬件相关】常见的模拟输出的硅麦
  • 免费SANS网络研讨会:IOC优先级评估与事件响应决策
  • 使用Amazon Bedrock和Amazon Transcribe构建AI驱动的自动化会议摘要系统
  • 【音频硬件相关】喇叭上的阻值和功率
  • 十木轻创:卖虚拟资料哪个平台好?小红书做这 5 个小项目,宝妈网上也能创业
  • 第二十二天
  • 十木轻创:有人偷偷挣了5.7个!干货全在这里。如何靠手机壁纸创收
  • 熔断降级(Go语言实现)
  • Vue + Node.js 全栈开发实战:构建现代化前端应用
  • Go语言的plugin
  • PandasAI连接LLM进行智能数据分析
  • 子序列中任意两个相邻元素的差值不超过 k的子序列个数
  • 低精度算术提升机器人定位效率 - 亚马逊科学团队技术创新
  • STM32F103C8T6芯片介绍(上) - LI,Yi
  • Lambda表达式你真的懂了嘛
  • DooTask 部署教程(windows)
  • KTT
  • AWS证书管理器现支持导出公钥证书 - 增强混合环境TLS管理能力
  • Go 源码编译流程