NSubstitute.Received(1)偶现误报多次调用,如何排查原因?
我使用NSubstitute编写测试用例,验证业务逻辑中方法的调用次数,测试代码如下:
await _memberAddUpdateService.Received(1).UpdateMemberNameAsync(member); await _customerAddUpdateService.DidNotReceive().AddCustomerAsync(member.Customer!); await _customerAddUpdateService.Received(1).UpdateCustomerAsync(member.Customer!); await _customerAddUpdateService.DidNotReceive().UpdateCustomerAsync(member.RfmsCustomer);
这些检查大多数时候正常,但偶尔会失败,提示相关方法被调用了两次。我确认项目中这些方法仅被调用一次,也不存在导致重复调用的循环逻辑。想知道是NSubstitute的Received调用存在异常,还是代码设计方面的因素导致方法被多次调用?
一、先排除NSubstitute相关的常见问题
测试隔离不足
如果测试类中的Substitute实例是类级别的字段,且未在每个测试方法执行前重新初始化(比如未在[SetUp]/[TestInitialize]中创建新实例),会导致前一次测试的调用记录被累积到下一次测试中,从而触发调用次数不符的错误。确保每个测试都使用全新的Substitute实例。异步方法的等待时机问题
若被测试逻辑中存在未正确await的异步操作(比如fire-and-forget的Task.Run、未await的方法调用),可能导致测试代码提前执行Received检查,而实际方法在检查完成后才完成第二次调用(比如异步任务延迟执行)。需确认被测试代码中所有异步调用都已正确await。参数匹配的隐性问题
NSubstitute的参数匹配是基于引用或值相等性的。如果实际调用时传入的参数与测试中member.Customer!不是同一个实例(比如业务逻辑中克隆了对象、重新实例化了Customer),可能导致NSubstitute误判调用次数:比如实际一次调用未匹配到测试中的参数,而其他无关调用被错误匹配,或者反过来,两次不同参数的调用被宽松匹配(比如用了Arg.Any<Customer>)算成了同一方法的调用。
二、代码设计/执行的隐藏因素
重复的事件订阅
若业务逻辑中存在事件订阅逻辑,且未正确避免重复订阅(比如在初始化方法中多次注册同一事件处理程序),会导致事件触发时方法被调用多次。检查是否存在重复订阅的场景。并发/多线程触发
业务逻辑若涉及并发操作(比如多线程、Parallel类、异步流的并发处理),可能在测试时因线程调度的随机性,导致方法被多次调用。这种情况下调用次数的异常是偶发的,与线程执行顺序有关。依赖注入的生命周期问题
如果被测试服务是单例生命周期,且测试环境未隔离单例实例(比如多个测试共享同一个服务实例),会导致调用次数在测试间累积,引发偶发的计数错误。
三、排查步骤
添加调用日志
在Substitute上添加回调,记录每次方法调用的细节,当测试失败时查看日志确认是否真的被调用两次:_customerAddUpdateService.When(x => x.UpdateCustomerAsync(Arg.Any<Customer>())) .Do(ctx => Console.WriteLine($"调用时间: {DateTime.Now}, 参数: {JsonConvert.SerializeObject(ctx.Arg<Customer>())}, 调用栈: {Environment.StackTrace}") );调试验证调用次数
在被测试方法的入口处打条件断点,统计实际调用次数,确认是否真的存在两次调用。强化测试隔离
确保每个测试方法执行前,所有依赖的Substitute和服务实例都被重新初始化,避免跨测试的状态污染。
内容的提问来源于stack exchange,提问作者Lotzi11

