You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 06:55:25