Moq 无法模拟静态方法、密封类或非虚成员,因其实质是通过生成继承子类来重写虚方法;必须使用接口或标记为 virtual/abstract/override 的成员。

Moq 不能模拟静态方法、密封类或非虚成员——这是你写完测试却始终报错 System.NotSupportedException: Invalid setup on a non-virtual member 的根本原因。必须确认被模拟类型是接口,或目标方法是 virtual / abstract / override。
为什么 Setup(x => x.Method()) 报错“non-virtual member”?
Moq 底层靠生成动态代理子类实现模拟,而 C# 不允许继承并重写非虚成员。
- 接口方法默认可被 Moq 实现,没问题
- 类中普通实例方法必须显式加
virtual才能被Setup -
sealed类完全无法被 Moq 模拟(哪怕它有 virtual 方法) - 静态方法、
private方法、internal方法(未开启 InternalsVisibleTo)一律不支持
常见误操作:直接 new Mock<productdataaccess>()</productdataaccess> —— 这个类如果没实现接口、也没声明任何 virtual,就会立刻失败。
Mock.Object 返回的是什么?
它返回一个实现了接口 T(或继承自类 T)的运行时代理对象,类型是 T,不是 Mock<t></t>。你必须把它传给被测类的构造函数或属性,而不是把 Mock<t></t> 实例本身塞进去。
- ✅ 正确:
var service = new OrderService(mockDb.Object, mockEmail.Object); - ❌ 错误:
var service = new OrderService(mockDb, mockEmail);(类型不匹配,编译不过) - ⚠️ 注意:
mockDb.Object是一次性快照;后续对mockDb.Setup()的修改不会影响已传入的对象,除非你重新创建Object
如何模拟异常、多次调用、不同参数返回不同值?
靠 Setup 链式配置 + It 匹配器组合实现,不是靠 if/else 写逻辑。
- 抛异常:
mock.Setup(x => x.Save(It.IsAny<order>())).Throws<invalidoperationexception>();</invalidoperationexception></order> - 按参数返回不同值:
mock.Setup(x => x.GetStatus("pending")).Returns("Processing"); mock.Setup(x => x.GetStatus("done")).Returns("Completed"); - 首次调用返回 A,第二次返回 B:
mock.SetupSequence(x => x.Next()).Returns("first").Returns("second"); - 验证是否被调用且参数匹配:
mock.Verify(x => x.Send("订单确认", It.Is<string>(s => s.Contains("@"))), Times.Once);</string>
注意 It.Is<T> 中的 lambda 是在验证阶段执行的,不是 Setup 阶段;别在里面写副作用代码。
Verify 调用次数时最容易漏掉的细节
Verify 默认只检查「是否按预期调用了」,但不会自动捕获「意外调用」。如果你没显式 Setup 某个方法,它默认返回默认值(false、null、0),且不会报错——这会让测试看似通过,实则掩盖了逻辑错误。
- 建议对关键依赖启用严格模式:
new Mock<IEmailService>(MockBehavior.Strict),此时任何未Setup的调用都会立刻抛MockException -
Times.Never和Times.AtLeastOnce很有用,但别滥用——过度断言会让测试脆弱,比如因重构内部调用次数而频繁失败 - 验证异步方法时,用
VerifyAsync(Moq 4.16+)或确保Setup返回Task并 await 被测方法
真实项目里,多数人栽在「以为 Verify 了就等于覆盖了所有路径」,其实只是证明了那条路径被执行了一次——参数边界、异常分支、并发场景,还得靠其他 Setup 组合补全。


















