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

C# MSTest异步单元测试本地正常,构建服务器执行失败求助

问题原因与解决方案

核心问题是**async void方法的测试时机不可控**:测试代码在执行GotoPageCommand.Execute后立刻执行验证,此时异步任务可能还未完成。本地和构建服务器的线程调度差异(比如服务器资源紧张、线程切换慢)导致本地刚好完成了异步操作,而服务器上异步任务还没执行到SearchPage调用,验证就已经执行了。

推荐解决方案:重构为可测试的异步命令

1. 重构ViewModel中的异步方法与命令

把async void的GotoPage改为async Task,并使用支持异步执行的命令(可自定义AsyncCommand或使用成熟框架的实现):

// 自定义AsyncCommand实现
public class AsyncCommand : ICommand
{
    private readonly Func<Task> _execute;
    private readonly Func<bool> _canExecute;
    private bool _isExecuting;

    public event EventHandler CanExecuteChanged;

    public AsyncCommand(Func<Task> execute, Func<bool> canExecute = null)
    {
        _execute = execute ?? throw new ArgumentNullException(nameof(execute));
        _canExecute = canExecute;
    }

    public bool CanExecute(object parameter)
    {
        return !_isExecuting && (_canExecute?.Invoke() ?? true);
    }

    public async void Execute(object parameter)
    {
        if (!CanExecute(parameter)) return;
        _isExecuting = true;
        try
        {
            await _execute();
        }
        finally
        {
            _isExecuting = false;
            RaiseCanExecuteChanged();
        }
    }

    // 暴露异步执行方法,供测试调用
    public async Task ExecuteAsync(object parameter)
    {
        if (!CanExecute(parameter)) return;
        _isExecuting = true;
        try
        {
            await _execute();
        }
        finally
        {
            _isExecuting = false;
            RaiseCanExecuteChanged();
        }
    }

    private void RaiseCanExecuteChanged()
    {
        CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }
}

// 修改ViewModel中的代码
public ICommand GotoPageCommand { get; }

// 构造函数初始化
GotoPageCommand = new AsyncCommand(GotoPage, CanGotoPage);

// 把async void改为async Task
private async Task GotoPage()
{
    // 其他逻辑...
    try
    {
        // 如果SearchPage是同步方法,无需用Task.Run,直接调用即可(减少线程调度不确定性)
        _pcCollection.PagData.SearchPage(page); 
        // 若SearchPage有异步版本,优先调用:await _pcCollection.PagData.SearchPageAsync(page);
    }
    catch (Exception ex)
    {
        // 添加异常处理(async void的异常难以捕获,async Task可在测试中捕获)
    }
}

2. 修改测试方法为异步测试,等待命令执行完成

将测试方法改为async Task类型,调用命令的异步执行方法并等待完成:

[TestMethod]
public async Task GotoPageCommand_Calls_PCCollection_SearchPage()
{
    var page = 100;
    var sut = CreateSut();
    sut.SearchPage = page;

    // 调用异步命令的ExecuteAsync方法并等待
    if (sut.GotoPageCommand is AsyncCommand asyncCmd)
    {
        await asyncCmd.ExecuteAsync(null);
    }
    else
    {
        throw new InvalidOperationException("命令类型不是AsyncCommand,请调整测试逻辑");
    }

    _tableViewModel.Verify(s => s.SearchPage(page), Times.Once());
}

临时应急方案(不推荐长期使用)

如果暂时无法重构代码,可在测试中添加等待逻辑,确保异步任务完成后再验证:

[TestMethod]
public void GotoPageCommand_Calls_PCCollection_SearchPage()
{
    var page = 100;
    var sut = CreateSut();
    sut.SearchPage = page;
    sut.GotoPageCommand.Execute(null);

    // 等待异步任务完成(时间需根据服务器性能调整,该方式不可靠)
    Task.Delay(2000).Wait();

    _tableViewModel.Verify(s => s.SearchPage(page), Times.Once());
}

额外优化建议

  • 移除不必要的Task.Run:如果SearchPage是同步方法,直接调用即可,避免线程上下文切换带来的不确定性。
  • 优先使用异步API:如果SearchPage有异步版本(如SearchPageAsync),直接调用并await,无需包装Task.Run。

内容的提问来源于stack exchange,提问作者Blingers

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 23:49:51