【发布时间】:2011-06-23 13:46:59
【问题描述】:
MVVM 的优点之一是 ViewModel 的可测试性。在我的特殊情况下,我有一个在调用命令时加载一些数据的 VM,以及它的相应测试:
public class MyViewModel
{
public DelegateCommand LoadDataCommand { get; set; }
private List<Data> myData;
public List<Data> MyData
{
get { return myData; }
set { myData = value; RaisePropertyChanged(() => MyData); }
}
public MyViewModel()
{
LoadDataCommand = new DelegateCommand(OnLoadData);
}
private void OnLoadData()
{
// loads data over wcf or db or whatever. doesn't matter from where...
MyData = wcfClient.LoadData();
}
}
[TestMethod]
public void LoadDataTest()
{
var vm = new MyViewModel();
vm.LoadDataCommand.Execute();
Assert.IsNotNull(vm.MyData);
}
所以这都是非常简单的东西。但是,我真正想做的是使用BackgroundWorker 加载数据,并在屏幕上显示“加载”消息。所以我把虚拟机改成:
private void OnLoadData()
{
IsBusy = true; // view is bound to IsBusy to show 'loading' message.
var bg = new BackgroundWorker();
bg.DoWork += (sender, e) =>
{
MyData = wcfClient.LoadData();
};
bg.RunWorkerCompleted += (sender, e) =>
{
IsBusy = false;
};
bg.RunWorkerAsync();
}
这在运行时视觉效果很好,但是我的测试现在失败了,因为没有立即加载属性。任何人都可以提出一个测试这种加载的好方法吗?我想我需要的是这样的:
[TestMethod]
public void LoadDataTest()
{
var vm = new MyViewModel();
vm.LoadDataCommand.Execute();
// wait a while and see if the data gets loaded.
for(int i = 0; i < 10; i++)
{
Thread.Sleep(100);
if(vm.MyData != null)
return; // success
}
Assert.Fail("Data not loaded in a reasonable time.");
}
然而,这看起来真的很笨重......它有效,但感觉很脏。 有更好的建议吗?
最终解决方案:
根据 David Hall 的回答,为了模拟 BackgroundWorker,我最终围绕 BackgroundWorker 做了一个相当简单的包装器,它定义了两个类,一个异步加载数据,一个同步加载。
public interface IWorker
{
void Run(DoWorkEventHandler doWork);
void Run(DoWorkEventHandler doWork, RunWorkerCompletedEventHandler onComplete);
}
public class AsyncWorker : IWorker
{
public void Run(DoWorkEventHandler doWork)
{
Run(doWork, null);
}
public void Run(DoWorkEventHandler doWork, RunWorkerCompletedEventHandler onComplete)
{
var bg = new BackgroundWorker();
bg.DoWork += doWork;
if(onComplete != null)
bg.RunWorkerCompleted += onComplete;
bg.RunWorkerAsync();
}
}
public class SyncWorker : IWorker
{
public void Run(DoWorkEventHandler doWork)
{
Run(doWork, null);
}
public void Run(DoWorkEventHandler doWork, RunWorkerCompletedEventHandler onComplete)
{
Exception error = null;
var args = new DoWorkEventArgs(null);
try
{
doWork(this, args);
}
catch (Exception ex)
{
error = ex;
throw;
}
finally
{
onComplete(this, new RunWorkerCompletedEventArgs(args.Result, error, args.Cancel));
}
}
}
那么在我的 Unity 配置中,我可以使用 SyncWorker 进行测试,使用 AsyncWorker 进行生产。然后我的 ViewModel 变成:
public class MyViewModel(IWorker bgWorker)
{
public void OnLoadData()
{
IsBusy = true;
bgWorker.Run(
(sender, e) =>
{
MyData = wcfClient.LoadData();
},
(sender, e) =>
{
IsBusy = false;
});
}
}
请注意,我标记为 wcfClient 的东西在我的测试中实际上也是一个 Mock,所以在调用 vm.LoadDataCommand.Execute() 之后,我还可以验证是否调用了 wcfClient.LoadData()。
【问题讨论】:
-
在 Caliburn 中添加 CoROutine 支持也会有所帮助。
-
"定义了两个类,一个异步加载数据,一个同步加载。"哇,那个模式听起来like something I've seen before...
-
@Will - 你提到这一点很有趣。在发布我的编辑后,我发现部分 Prism 指南谈到使用
SynchronizationContext但它的编写方式对我来说只有 1/2 有意义,并且没有真正提到测试。我必须找到一个使用它的好例子,看看是否可以代替它。 -
实际上,Prism 指南还提到了使用 WPF 的
Dispatcher,这里也提到了:stackoverflow.com/questions/1949789/…,但这似乎不太可测试。 -
您的最终解决方案对我帮助很大。谢谢。
标签: c# wpf unit-testing mvvm backgroundworker