Moq的Mock<T>不能直接new,因其是动态代理包装器,必须通过.Object获取模拟实例;Setup用于方法,SetupGet用于只读属性,参数匹配应使用It.Is<T>()而非硬编码值。

Moq 不是“必须用”的 Mock 框架,但它是 .NET 生态中最轻量、语法最直观、与 xUnit/NUnit 集成最顺的选项——尤其适合刚写单元测试、又不想被复杂生命周期和配置绊住的人。
为什么 Moq 的 Mock<T> 不能直接 new?
因为 Mock<T> 是一个包装器,它背后生成的是运行时动态代理(不是 T 本身),所以你不能把它当真实对象传给被测方法——除非你显式暴露 Object 属性。
- 错误写法:
var mock = new Mock<IRepository>(); SomeMethod(mock);→ 编译失败,类型不匹配 - 正确写法:
SomeMethod(mock.Object);→.Object才是模拟出的接口实例 - 漏掉
.Object是新手最常卡住的地方,报错通常是Cannot convert from 'Mock<IRepository>' to 'IRepository'
Setup() 和 SetupGet() 到底该选哪个?
取决于你要模拟的是方法调用,还是只读属性访问。Moq 对这两类成员的设置逻辑不同,混用会静默失效或抛 NotSupportedException。
- 模拟方法:
mock.Setup(x => x.GetById(123)).Returns(new User()); - 模拟只读属性:
mock.SetupGet(x => x.Name).Returns("Alice");(注意不是Setup(x => x.Name)) - 模拟可写属性的 set:用
SetupSet,但极少需要——单元测试里应避免验证 setter 被调用,而应验证状态变更结果 - 如果对属性用了
Setup却没生效,八成是该用SetupGet或SetupSet
怎么让 Moq 返回不同值,对应不同入参?
靠 It.Is<T>() + lambda 匹配,而不是硬写死参数。否则容易因浮点误差、引用相等、null 判断翻车。
- 基础匹配:
mock.Setup(x => x.Find(It.Is<string>(s => s.StartsWith("A")))).Returns(...); - 忽略参数(任意值都命中):
mock.Setup(x => x.Save(It.IsAny<User>())).Returns(true); - 带副作用的匹配(比如记录调用次数):
mock.Setup(x => x.Log(It.IsAny<string>())).Callback<string>(s => calls.Add(s)); - 别用
Setup(x => x.GetById(123)).Returns(...)去覆盖Setup(x => x.GetById(456))——Moq 会按注册顺序匹配,后注册的不会覆盖前注册的
Mock 验证失败时,Verify() 报错信息太模糊怎么办?
默认只告诉你“Expected invocation on the mock once, but was 0 times”,不告诉你哪次调用没发生。加 Times.Once() 是必须的,但还不够。
- 明确指定次数:
mock.Verify(x => x.Delete(123), Times.Once());,比Verify()无参调用更精准 - 验证“从未调用”:
mock.Verify(x => x.SendEmail(It.IsAny<string>()), Times.Never()); - 验证至少一次:
Times.AtLeastOnce(),但慎用——它掩盖了过度调用问题 - 如果 Verify 失败却找不到原因,打开
mock.Invocations看实际发生了什么调用(调试时打印出来),比猜快得多
Mock 的本质是控制依赖行为,不是构造“看起来像”的对象。最容易被忽略的是:一旦开始 Setup 行为,就要为所有可能路径提供返回值或异常,否则遇到未 Setup 的调用会直接抛 NullReferenceException 或进入默认行为(如返回 null、0、false),这往往比报错更难排查。


















