Skip to content

Instantly share code, notes, and snippets.

@cuteribs
Last active November 7, 2025 07:46
Show Gist options
  • Select an option

  • Save cuteribs/56dddf7577eb0c90b81718f775dfd8cc to your computer and use it in GitHub Desktop.

Select an option

Save cuteribs/56dddf7577eb0c90b81718f775dfd8cc to your computer and use it in GitHub Desktop.

Moq 与 NSubstitute 详细对比

在 .NET 的单元测试世界中,Moq 和 NSubstitute 是两个最受欢迎的模拟(Mocking)框架。它们都旨在帮助开发者创建测试替身(Test Doubles),以便在隔离的环境中测试代码单元。尽管目标相同,但它们在设计理念、API 语法和使用体验上存在显著差异。

1. 核心设计理念

这是两者最根本的区别,影响了它们的一切。

  • Moq: "录制-回放/期望" 模式 (Record-Replay/Expectation)

    • 理念: Moq 的 API 围绕着“设置期望 (Setup)”和“验证 (Verify)”这两个核心概念构建。你首先要明确地告诉 mock 对象:“当你接收到某个特定调用时,你应该如何反应”。然后,在测试的最后,你可以验证这个调用是否真的发生了。
    • 感觉: 像是在编写一个脚本。你先为 mock 对象定义好所有规则,然后执行测试逻辑,最后检查 mock 是否按脚本演出了。API 结构非常严谨和明确。
  • NSubstitute: "替代品/状态" 模式 (Substitute/State-Based)

    • 理念: NSubstitute 旨在创建一个行为更像真实对象的“替代品”。你不是在设置严格的期望,而是在配置这个替代品的状态和行为。它更关注“当……时,返回……”,而不是强制性的“我期望……被调用”。验证调用也是事后检查,感觉更自然。
    • 感觉: 像是在与一个真实的、可配置的对象交互。它的语法更接近自然语言,减少了“仪式感”,让开发者更专注于测试逻辑本身。

2. 语法和用法对比 (代码示例)

我们将通过常见的测试场景来对比两者的语法。假设我们有以下接口:

public interface ICalculator
{
    int Add(int a, int b);
    string Mode { get; set; }
    void PowerOn();
    event EventHandler PoweringUp;
}

场景一:创建 Mock/Substitute

  • Moq:

    var mockCalculator = new Mock<ICalculator>();
    ICalculator calculator = mockCalculator.Object; // 必须通过 .Object 属性获取实例
  • NSubstitute:

    ICalculator calculator = Substitute.For<ICalculator>(); // 直接返回实例

    对比: NSubstitute 更简洁,一步到位。Moq 则将 mock 的配置对象 (Mock<T>) 和实际使用的对象 (.Object) 分开,这在某些高级场景下更灵活,但日常使用稍显繁琐。

场景二:设置返回值 (Stubbing)

  • Moq: 使用 SetupReturns Lambda 表达式。

    mockCalculator.Setup(c => c.Add(1, 2)).Returns(3);
    
    // 测试代码
    var result = calculator.Add(1, 2); // result会是3
  • NSubstitute: 直接在方法调用后使用 .Returns()

    calculator.Add(1, 2).Returns(3);
    
    // 测试代码
    var result = calculator.Add(1, 2); // result会是3

    对比: NSubstitute 的流式 API(Fluent API)在这里的可读性通常被认为更高,更接近自然语言。

场景三:参数匹配

当你不关心传入的具体参数时。

  • Moq: 使用 It 类。

    // 匹配任意整数
    mockCalculator.Setup(c => c.Add(It.IsAny<int>(), It.IsAny<int>())).Returns(100);
    
    // 匹配满足条件的参数
    mockCalculator.Setup(c => c.Add(It.Is<int>(i => i > 0), 5)).Returns(10);
  • NSubstitute: 使用 Arg 类。

    // 匹配任意整数
    calculator.Add(Arg.Any<int>(), Arg.Any<int>()).Returns(100);
    
    // 匹配满足条件的参数
    calculator.Add(Arg.Is<int>(i => i > 0), 5).Returns(10);

    对比: 语法非常相似,只是静态类的名字不同 (It vs Arg)。两者功能上基本等价。

场景四:验证方法调用 (Verification)

  • Moq: 使用 Verify 方法,非常强大和精确。

    // 验证 Add(1, 2) 被调用了一次
    mockCalculator.Verify(c => c.Add(1, 2), Times.Once());
    
    // 验证 PowerOn 被调用了至少两次
    mockCalculator.Verify(c => c.PowerOn(), Times.AtLeast(2));
    
    // 验证 Add 方法从未被任何负数调用
    mockCalculator.Verify(c => c.Add(It.Is<int>(i => i < 0), It.IsAny<int>()), Times.Never());
  • NSubstitute: 使用 Received()DidNotReceive()

    // 验证 Add(1, 2) 被调用了 (默认至少一次)
    calculator.Received().Add(1, 2);
    
    // 验证 PowerOn 被调用了两次
    calculator.Received(2).PowerOn();
    
    // 验证 Add 方法从未被任何负数调用
    calculator.DidNotReceive().Add(Arg.Is<int>(i => i < 0), Arg.Any<int>());

    对比: NSubstitute 的语法再次显得更流畅自然。Moq 的 Times 枚举功能非常强大,可以进行非常复杂的调用次数验证,而 NSubstitute 通过 Received(N) 也能满足绝大多数需求。

场景五:属性操作

  • Moq:

    // 设置属性返回值
    mockCalculator.Setup(c => c.Mode).Returns(\"Decimal\");
    
    // 验证属性被设置
    mockCalculator.SetupSet(c => c.Mode = \"Hex\").Verifiable();
    // ...
    mockCalculator.Verify();
    
    // 自动追踪属性值变化
    mockCalculator.SetupAllProperties(); 
    calculator.Mode = \"Binary\";
    Assert.AreEqual(\"Binary\", calculator.Mode);
  • NSubstitute:

    // 设置属性返回值
    calculator.Mode.Returns(\"Decimal\");
    
    // NSubstitute 默认就会像真实对象一样追踪属性值,无需额外设置
    calculator.Mode = \"Binary\";
    Assert.AreEqual(\"Binary\", calculator.Mode);
    
    // 验证属性被设置
    calculator.Received().Mode = \"Binary\";

    对比: NSubstitute 在处理属性时极其简单直观,其行为更符合人们对一个普通对象的预期。Moq 需要更多的显式配置。

3. 关键特性与差异总结

特性 Moq NSubstitute 备注
API 风格 基于 Lambda 表达式的 SetupVerify 流式(Fluent)、接近自然语言 NSubstitute 通常被认为学习曲线更平缓,代码可读性更高。
严格/松散模式 支持严格模式 (MockBehavior.Strict),任何未经 Setup 的调用都会抛出异常。 默认松散。所有未配置的方法/属性默认返回 nulldefault(T)。没有内置的严格模式。 Moq 的严格模式有助于发现意外的依赖调用,但也会让测试变得更脆弱。
非虚方法/静态方法 不支持(需要商业版的 Moq.AutoMocker 或其他工具如 Pose, Harmony 不支持 这是所有基于代理的 mocking 框架的共同限制。
学习曲线 稍陡峭,需要理解 Setup/Verify 的概念。 非常平缓,API 直观易懂。 新手通常能更快地掌握 NSubstitute。
社区与维护 历史上是 .NET 社区的绝对王者,用户基数巨大。 社区活跃,非常受欢迎,近年来使用者增长迅速。 重要: 见下文的“SponsorLink 争议”。
灵活性与控制 提供了非常精细的控制,如 SetupSequence, Verifiable, Callback 等。 API 设计更简洁,虽然也支持回调等高级功能,但总体上更注重易用性。 Moq 在处理极其复杂的模拟场景时可能提供更多底层控制。

4. Moq 的 "SponsorLink" 争议 (非常重要)

在 2023 年 8 月,Moq 4.20.0 版本引入了一个名为 SponsorLink 的依赖。这个依赖会在编译时收集开发者邮箱的 SHA-256 哈希值并上传到其服务器,以验证用户是否是 GitHub Sponsor。这一行为在未经用户明确同意的情况下收集数据,引发了社区的强烈反弹和信任危机。

  • 影响:
    1. 信任破裂: 许多开发者和公司因隐私和安全顾虑而放弃或禁止使用 Moq。
    2. 版本锁定: 很多项目选择锁定在 4.20.0 之前的版本(如 4.18.4)。
    3. 替代品崛起: 这次事件极大地推动了 NSubstitute 和 FakeItEasy 等替代品的采用率。

尽管 Moq 的作者后来移除了这个具有争议的功能,但其声誉已经受到了严重损害。

5. 结论与推荐

  • 如果你是新手或开始一个新项目: 强烈推荐 NSubstitute。它的学习曲线平缓,API 优雅且可读性高,能让你更专注于编写测试逻辑而不是模拟框架的语法。此外,它没有历史包袱和信任问题,社区健康活跃。

  • 如果你正在维护一个大量使用 Moq 的旧项目:

    • 迁移成本高: 全部替换成 NSubstitute 可能是一项巨大的工程。
    • 建议:
      1. 将项目中的 Moq 版本锁定在 4.18.4 或更早,以避免 SponsorLink 争议。
      2. 对于项目中新增的测试,可以考虑开始使用 NSubstitute,逐步过渡。
      3. 评估迁移的可行性。如果项目会长期维护,长痛不如短痛,制定计划逐步迁移是值得的。
  • 为什么还会有人选择 Moq?:

    1. 习惯与存量: 它是事实上的“前任标准”,无数的教程、博客和现有代码库都在使用它。
    2. 强大的控制力: 对于某些需要极其精细控制的复杂测试场景,Moq 的 Setup API 仍然非常强大。
    3. 严格模式: 如果你的团队非常依赖严格模式来规范测试,Moq 是原生支持的。

总而言之,在当前(2023年后)的 .NET 生态中,NSubstitute 因其简洁的设计、优秀的开发者体验以及可靠的社区声誉,已成为大多数项目(尤其是新项目)的首选。Moq 依然是一个功能强大的框架,但其近期的争议使其推荐度大大降低。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment