nsubstitute 是语义更直白、链式调用更自然的 mock 框架,需用 substitute.for() 创建模拟对象,统一用 .returns() 模拟方法和属性,验证必须显式调用 .received() 或 .didnotreceive()。

NSubstitute 不是 Moq 的“轻量替代品”,它是另一套语义更直白、链式调用更自然的 Mock 框架——你不用记 Setup 和 SetupGet 的区别,也不用纠结 .Object 该不该写。
NSubstitute 创建模拟对象必须用 Substitute.For<t>()</t>
它不提供构造函数直接实例化,所有模拟对象都必须通过这个工厂方法生成。这不是限制,而是设计选择:避免用户误把 Mock<t></t> 当真实类型传入被测类。
-
Substitute.For<iapiservice>()</iapiservice>返回的就是IApiService类型,可直接注入,无需额外属性提取 - 接口有泛型参数?照写:
Substitute.For<irepository>>()</irepository> - 要模拟类(非接口)?得加
CallBase = true配置,否则虚方法不会自动调用基类实现 - 别试图
new NSubstitute()或new Substitute<t>()</t>—— 类不存在,编译直接报错
方法/属性模拟统一用点号调用 + .Returns()
NSubstitute 把“设置行为”和“调用语法”对齐了:你写什么,它就 mock 什么。没有 SetupGet,也没有 SetupSet,只有 .Returns() 和 .ReturnsForAnyArgs()。
- 模拟方法:
apiService.Login(Arg.Any<user>()).Returns(true)</user> - 模拟只读属性:
apiService.Name.Returns("Alice")(注意不是apiService.GetName()) - 模拟可写属性的 getter:同样用
.Returns();setter 默认忽略,除非你显式用.When(x => x.Name = "Bob").Do(...) - 如果写了
apiService.Login(new User()).Returns(false),但测试时传的是另一个User实例,它不会命中——要用Arg.Is<user>(u => u.Id == 123)</user>或Arg.Any<user>()</user>
验证调用必须显式写 .Received() 或 .DidNotReceive()
NSubstitute 不在执行后自动校验,也不依赖 Verify() 方法。你得主动问:“它被调用了没?”——这反而让断言意图更清晰。
- 检查是否调用过:
apiService.Received().Login(Arg.Is<user>(u => u.UserName == "admin"))</user> - 检查调用次数:
apiService.Received(2).Save(Arg.Any<user>())</user> - 检查没被调用:
apiService.DidNotReceive().Delete(999) - 漏写
.Received()就等于没验证——测试会绿,但实际逻辑可能根本没触发依赖
参数匹配器写错会导致模拟静默失效
NSubstitute 的 Arg.Any<t>()</t> 和 Arg.Is<t>()</t> 是运行时匹配,不是编译期约束。写错条件或类型不一致,它不会报错,只是跳过匹配,走默认返回值(通常是 default(T))。
-
Arg.Is<int>(x => x > 0)</int>对string参数无效,但也不会提醒你 -
Arg.Any<object>()</object>能匹配一切,但会掩盖类型意图,建议精确到具体类型 - 用
Arg.Do<t>(action)</t>做副作用(如记录参数),但别在里面抛异常——它会中断整个调用链 - 多个重载方法?NSubstitute 会按签名严格匹配,
Login(User)和Login(string, string)是完全独立的模拟目标
NSubstitute 最容易被忽略的一点:它默认不拦截非虚成员。如果你 mock 了一个类(而非接口),又去调用它的非 virtual 方法,那跑的还是真实逻辑——这点不像 Moq 会直接抛异常提醒你,它就默默执行,直到你发现测试结果不对才回头排查。











