基于对象属性而非引用设置Mock返回类型的NSubstitute单元测试问题
解决NSubstitute中Mock内部new对象的匹配问题
我太懂这个痛点了!当被测试方法里自己new了一个对象传给依赖的构建器时,NSubstitute默认的引用匹配确实会让我们的Mock配置直接失效——毕竟方法内部new出来的对象和我们在测试代码里创建的对象引用完全不一样,NSubstitute认不出来。
问题核心
NSubstitute默认对方法参数采用引用相等匹配:只有当传入的参数和你配置Mock时用的参数是同一个对象(引用完全一致),才会触发你设置的返回值。但如果被测试方法内部是new Request { ... }这种方式创建对象,测试代码里的对象和它根本不是同一个引用,自然匹配不上。
解决方案:用参数匹配器实现属性匹配
NSubstitute提供了灵活的参数匹配器,我们可以用Arg.Is<T>(predicate)来指定“只要参数满足某个属性条件,就触发Mock的返回”,完全绕开引用匹配的限制。
举个实际的代码例子:
假设被测试的业务代码是这样的:
public class OrderProcessor { private readonly IOrderBuilder _orderBuilder; public OrderProcessor(IOrderBuilder orderBuilder) { _orderBuilder = orderBuilder; } public OrderResult CreateOrder(int customerId, decimal amount) { // 内部new了一个OrderRequest对象 var orderRequest = new OrderRequest { CustomerId = customerId, Amount = amount }; return _orderBuilder.BuildOrder(orderRequest); } } public interface IOrderBuilder { OrderResult BuildOrder(OrderRequest request); } public class OrderRequest { public int CustomerId { get; set; } public decimal Amount { get; set; } } public class OrderResult { public bool IsSuccess { get; set; } }
错误的Mock写法(不会生效)
如果你直接这么写,测试会失败,因为引用不匹配:
var mockBuilder = Substitute.For<IOrderBuilder>(); // 这里的OrderRequest和方法内部new的不是同一个引用 mockBuilder.BuildOrder(new OrderRequest { CustomerId = 123, Amount = 99.9m }) .Returns(new OrderResult { IsSuccess = true }); var processor = new OrderProcessor(mockBuilder); var result = processor.CreateOrder(123, 99.9m); // result会是null,匹配失败
正确的Mock写法(用Arg.Is匹配属性)
改用Arg.Is指定属性条件,就能完美匹配内部new的对象:
var mockBuilder = Substitute.For<IOrderBuilder>(); // 只要传入的OrderRequest的CustomerId是123且Amount是99.9m,就返回成功结果 mockBuilder.BuildOrder(Arg.Is<OrderRequest>(req => req.CustomerId == 123 && req.Amount == 99.9m)) .Returns(new OrderResult { IsSuccess = true }); var processor = new OrderProcessor(mockBuilder); var result = processor.CreateOrder(123, 99.9m); // 现在能正确拿到IsSuccess=true的结果了
进阶用法
如果不需要精确匹配所有属性,还可以:
- 用
Arg.Any<OrderRequest>()匹配任何类型为OrderRequest的参数:mockBuilder.BuildOrder(Arg.Any<OrderRequest>()) .Returns(new OrderResult { IsSuccess = true }); - 结合多个条件灵活匹配,比如忽略某个属性:
// 只要CustomerId是123,不管Amount是多少都返回成功 mockBuilder.BuildOrder(Arg.Is<OrderRequest>(req => req.CustomerId == 123)) .Returns(new OrderResult { IsSuccess = true });
补充:不推荐重写Equals
虽然重写OrderRequest的Equals和GetHashCode方法也能让NSubstitute按值匹配,但这种方式会侵入业务代码,可能对其他逻辑造成意外影响,所以更推荐用参数匹配器的方式,更灵活且安全。
内容的提问来源于stack exchange,提问作者Buda Gavril
相关产品推荐
相关产品推荐

