Одной из приятных особенностей MVVM является тестируемость ViewModel. В моем конкретном случае у меня есть виртуальная машина, которая загружает некоторые данные при вызове команды и соответствующий ей тест:
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.");
}
Однако это кажется действительно неуклюжим ... Это работает, но просто кажется грязным.
Есть лучшие предложения?
Возможное решение :
Основываясь на ответе Дэвида Холла, чтобы смоделировать 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
, на самом деле тоже является Мок в моих тестах, поэтому после вызова vm.LoadDataCommand.Execute()
я также могу проверить, что wcfClient.LoadData()
был вызван.