如何在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中,这非常有效。
。正如目前所写的,"初始化"任务被忽略了。因此,没有错误处理,也没有向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");
}