Одна из приятных особенностей 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
, на самом деле также является Mock в моих тестах, поэтому после вызова vm.LoadDataCommand.Execute ()
Я также могу подтвердить, что был вызван wcfClient.LoadData ()
.