如何测试使用BackgroundWorker加载的ViewModel
本文关键字:BackgroundWorker 加载 ViewModel 何测试 测试 | 更新日期: 2023-09-27 18:00:02
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
加载数据,并在屏幕上显示"加载"消息。所以我将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()
之后,我也可以验证是否调用了wcfClient.LoadData()
。
引入一个mock/fake后台工作程序,它验证您是否正确调用了它,但会立即返回一个固定的响应。
更改视图模型以允许注入依赖项,可以通过属性注入或构造函数注入(我在下面显示构造函数注入),然后在测试时传入伪后台工作程序。在现实世界中,您在创建虚拟机时注入了真实的实现。
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),连接正确的后台工作人员应该不会太难。
这种方法的一个问题是,包括BackGroundWorker在内的大多数Microsoft类都没有实现接口,因此伪造/嘲笑它们可能很棘手。
我发现的最好的方法是为要模拟的对象创建自己的接口,然后在实际的Microsoft类的顶部创建一个包装器对象。这并不理想,因为你有一薄层未经测试的代码,但至少这意味着你的应用程序的未经测试表面进入了测试框架,远离了应用程序代码。
如果您愿意用它来换取少量的视图模型污染(即引入仅用于测试的代码),您可以避免额外的抽象,如下所示:
首先,将可选的AutoResetEvent(或ManualResetEvent)添加到视图模型构造函数中,并确保在后台工作程序完成"RunWorkerCompleted"处理程序时"设置"此AutoResetEvents实例。
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();
}
}
现在,您可以通过一个实例作为单元测试的一部分。
[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)类的设计目的。因此,除了轻微的视图模型代码污染之外,我认为这个解决方案非常简洁。