如何测试使用BackgroundWorker加载的ViewModel?

MVVM的一个好处是ViewModel的可测试性。 在我的特定情况下,我有一个VM在调用命令时加载一些数据,以及相应的测试:

public class MyViewModel { public DelegateCommand LoadDataCommand { get; set; } private List myData; public List 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加载数据,并在屏幕上显示“加载”消息。 所以我将VM更改为:

 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()我还可以validation是否调用了wcfClient.LoadData()

介绍一个模拟/伪造的后台工作程序,它validation您是否正确调用它,但立即返回一个预设响应。

更改您的视图模型以允许注入依赖项,通过属性注入或构造函数注入(我在下面显示构造函数注入),然后在测试时传入伪后台工作程序。 在现实世界中,您在创建VM时注入了真实的实现。

 public class MyViewModel { private IBackgroundWorker _bgworker; public MyViewModel(IBackgroundWorker bgworker) { _bgworker = bgworker; } private void OnLoadData() { IsBusy = true; // view is bound to IsBusy to show 'loading' message. _bgworker.DoWork += (sender, e) => { MyData = wcfClient.LoadData(); }; _bgworker.RunWorkerCompleted += (sender, e) => { IsBusy = false; }; _bgworker.RunWorkerAsync(); } } 

根据您的框架(在您的情况下为Unity / Prism),连接正确的后台工作人员不应该太难。

这种方法的一个问题是大多数Microsoft类(包括BackGroundWorker)都没有实现接口,因此伪造/模拟它们可能会很棘手。

我发现的最好的方法是为要模拟的对象创建自己的接口,然后创建一个位于实际Microsoft类之上的包装器对象。 不太理想,因为你有一层未经测试的代码,但至少这意味着你的应用程序未经测试的表面会进入测试框架并远离应用程序代码。

如果您愿意为少量的视图模型污染(即引入仅用于测试的代码)进行交易,您可以避免额外的抽象,如下所示:

首先将可选的AutoResetEvent(或ManualResetEvent)添加到视图模型构造函数,并确保在后台工作程序完成“RunWorkerCompleted”处理程序时“设置”此AutoResetEvent实例。

 public class MyViewModel { private readonly BackgroundWorker _bgWorker; private readonly AutoResetEvent _bgWorkerWaitHandle; public MyViewModel(AutoResetEvent bgWorkerWaitHandle = null) { _bgWorkerWaitHandle = bgWorkerWaitHandle; _bgWorker = new BackgroundWorker(); _bgWorker.DoWork += (sender, e) => { //Do your work }; _bgworker.RunWorkerCompleted += (sender, e) => { //Configure view model with results if (_bgWorkerWaitHandle != null) { _bgWorkerWaitHandle.Set(); } }; _bgWorker.RunWorkerAsync(); } } 

现在,您可以将实例作为unit testing的一部分传入。

 [Test] public void Can_Create_View_Model() { var bgWorkerWaitHandle = new AutoResetEvent(false); //Make sure it starts off non-signaled var viewModel = new MyViewModel(bgWorkerWaitHandle); var didReceiveSignal = bgWorkerWaitHandle.WaitOne(TimeSpan.FromSeconds(5)); Assert.IsTrue(didReceiveSignal, "The test timed out waiting for the background worker to complete."); //Any other test assertions } 

这正是AutoResetEvent(和ManualResetEvent)类的设计目的。 因此,除了轻微的模型代码污染,我认为这个解决方案是非常整洁的。