使用NSubstitute模拟异步方法依赖时,调用方无法收回控制权
NSubstitute模拟异步方法导致测试卡住问题分析
在测试包含while循环的异步方法时,用NSubstitute模拟依赖的Wait()异步方法:
- 返回
Task.CompletedTask,测试直接卡住,无法触发CancellationToken取消操作; - 返回
Task.Delay(10),测试能正常完成; - 返回
Task.Delay(1),测试还是卡住。
怀疑问题和任务调度器的线程处理逻辑有关。
待测试类与依赖接口
public class ClassUnderTest { private readonly IDependencyToBeMocked _dependency; public ClassUnderTest(IDependencyToBeMocked dependency) { _dependency = dependency; } // 注意:原代码中stoppingToken应为参数token,否则编译错误 public async Task MethodToBeTested(CancellationToken token) { while (!token.IsCancellationRequested) { await _dependency.Wait(); } } } public interface IDependencyToBeMocked { Task Wait(); }
测试代码
public class Tests { private readonly IDependencyToBeMocked _dependency; private readonly ClassUnderTest _classUnderTest; public Tests() { _dependency = Substitute.For<IDependencyToBeMocked>(); _classUnderTest = new ClassUnderTest(_dependency); } [Fact] public async Task Test() { // arrange var source = new CancellationTokenSource(); _dependency.Wait().Returns(Task.CompletedTask); // act var task = _classUnderTest.MethodToBeTested(source.Token); await source.CancelAsync(); await task; // assert // do some assertions } }
问题原因
- 返回
Task.CompletedTask时卡住:await已完成的任务会直接同步执行后续代码,不会让出当前线程。while循环变成无限同步循环,一直占用线程执行循环检查,完全没给source.CancelAsync()的执行留时间,取消操作永远触发不了,测试卡住。 - 返回
Task.Delay(10)正常:await Task.Delay(10)会让出当前线程,线程可以去处理source.CancelAsync(),取消令牌触发后循环退出,测试正常完成。 - 返回
Task.Delay(1)仍卡住:延迟时间太短,线程还没来得及切换执行取消逻辑,循环就已经进入下一轮,导致还是卡住。
解决方法
让模拟的Wait()返回一个会让出线程的异步任务,确保取消操作有机会执行,推荐以下几种方式:
方式1:用Task.Yield()
_dependency.Wait().Returns(Task.Yield());
Task.Yield()会强制将后续代码调度到线程池执行,让出当前线程,给取消操作留出执行时间。
方式2:返回Task.Delay(0)
_dependency.Wait().Returns(Task.Delay(0));
Task.Delay(0)同样会让出线程,让调度器有机会处理其他任务。
方式3:用异步lambda返回任务
_dependency.Wait().Returns(async () => await Task.CompletedTask);
通过异步lambda生成状态机,确保await时会触发异步调度流程。
另外务必修正待测试方法中的笔误:将stoppingToken改为方法参数token,否则取消令牌无法被正确检查。
内容的提问来源于stack exchange,提问作者not_only_but_also
相关产品推荐
相关产品推荐

