我已经创建了一个示例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中,这就像一个魅力。 当我想为这个viewmodel创建一个单元测试时,我通过调用它的默认构造函数来创建该对象:
[TestMethod]
public void Test()
{
customerOverviewViewModel = new CustomerOverviewViewModel();
// Do the test
}
但是,单元测试无法知道异步方法何时完成。 可以使用构造函数初始化来修复它,还是应该使用不同的模式?
修改
单元测试不需要异步加载信息,他们只需要一个类的实例来测试方法。 仅仅为我的单元测试使用另一种初始化方法似乎需要做很多额外的工作。
所有单元测试都成功但有时会抛出多个线程尝试访问相同上下文的错误(当数据未加载异步时消失):
创建模型时无法使用上下文。如果在OnModelCreating方法中使用上下文,或者同时由多个线程访问相同的上下文实例,则可能抛出此异常。请注意,DbContext和相关类的实例成员不保证是线程安全的。
答案 0 :(得分:4)
在WPF中,这就像魅力一样。
排序。如目前所写,“初始化”任务只是被忽略。因此,没有错误处理,并且UI没有指示初始化正在进行或已完成(即,它无法知道何时显示微调器)。
换句话说,显示此信息(状态以及结果数据)不仅仅对单元测试代码有用。我有一个简单的data-bindable task wrapper,我刚才写过这样的情况来帮助。
如果异步方法返回Task
,则没有其他方法可以检测异步方法的完成情况。你不得不以某种方式揭露Task
。我认为暴露它的最佳方式就像我写的类型,所以UI也可以使用它。
答案 1 :(得分:3)
简而言之,您的模型可能如下所示:
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");
}