如何在WPF中使用异步初始化对ViewModel进行单元测试

本文关键字:初始化 ViewModel 单元测试 异步 WPF | 更新日期: 2023-09-27 18:01:28

我已经创建了一个样例WPF MVVM项目,我现在要进行单元测试。视图模型在构造函数中异步加载数据:

public class CustomerOverviewViewModel
{
   public CustomerOverviewViewModel()
   {
       var t = LoadFullCustomerListAsync();
   }
   public async Task LoadFullCustomerListAsync()
   {
      List<BL_acc> customers = await Task.Run(() => // Query from db);
   }
}

在WPF中,这非常有效。当我想为这个视图模型创建一个单元测试时,我通过调用它的默认构造函数来创建对象:

  [TestMethod]
  public void Test()
  {
      customerOverviewViewModel = new CustomerOverviewViewModel();
      // Do the test
  }

但是,单元测试无法知道异步方法何时完成。可以使用构造函数初始化来修复这个问题吗?还是应该使用不同的模式?

编辑

单元测试不需要异步加载的信息,它们只需要一个类的实例来测试方法。对于我的单元测试来说,使用另一个初始化方法似乎要做很多额外的工作。

所有的单元测试都成功了,但它们有时会抛出一个错误,多个线程试图访问同一个上下文(当数据不是异步加载时就会消失):

在创建模型时不能使用上下文。如果上下文在OnModelCreating方法中使用,或者多个线程同时访问同一个上下文实例,则可能引发此异常。请注意,DbContext的实例成员和相关类不能保证是线程安全的。

如何在WPF中使用异步初始化对ViewModel进行单元测试

在WPF中,这非常有效。

。正如目前所写的,"初始化"任务被忽略了。因此,没有错误处理,也没有向UI指示初始化正在进行或完成(即,它无法知道何时显示旋转器)。

换句话说,显示这些信息(状态以及结果数据)将不仅仅对单元测试代码有用。我有一个简单的数据绑定任务包装器,它是我不久前写的,可以帮助处理这种情况。

如果异步方法返回一个Task,没有其他方法可以检测异步方法的完成;你必须以某种方式暴露Task。我认为最好的公开方式是类似我写的类型,这样UI也可以利用它。

Stephen的回答包含了所有相关信息。我仍然要完成我的演示,以展示如何实际地对异步ViewModel进行单元测试。

简单地说,你的模型可能是这样的:
public class CustomerOverviewViewModel : INotifyPropertyChanged 
{
    readonly Task<string> _dataTask;
    readonly Task _notifierTask;
    public event PropertyChangedEventHandler PropertyChanged = delegate {};
    public string Data
    {
        get
        {
            return _dataTask.IsCompleted ? 
                _dataTask.GetAwaiter().GetResult() : 
                "loading...";
        }
    }
    public CustomerOverviewViewModel()
    {
        _dataTask = LoadFullCustomerListAsync();
        Func<Task> notifier = async () =>
        {
            try 
            {           
                await _dataTask;
            }
            finally
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs("Data"));
            }
        };
        // any exception thrown by _dataTask stays dormant in _notifierTask
        // and may go unobserved until GC'ed, but it will be re-thrown 
        // to the whenever CustomerOverviewViewModel.Data is accessed
        // from PropertyChanged event handlers
        _notifierTask = notifier(); 
    }
    async Task<string> LoadFullCustomerListAsync()
    {
        await Task.Delay(1000);
        return "42";
    }
}

你的单元测试现在也是异步的:

[TestMethod]
public async Task Test()
{
    var customerOverviewViewModel = new CustomerOverviewViewModel();
    var tcs = new TaskCompletionSource<bool>();
    PropertyChangedEventHandler handler = (s, e) =>
    {
        if (e.PropertyName == "Data")
            tcs.TrySetResult(true);
    };
    customerOverviewViewModel.PropertyChanged += handler;
    try 
    {
        await tcs.Task;
    }
    finally
    {
        customerOverviewViewModel.PropertyChanged -= handler;
    }
    Assert.IsTrue(customerOverviewViewModel.Data == "42");
}