Comment tester un ViewModel chargé avec un BackgroundWorker?

L’un des avantages de MVVM est la testabilité du ViewModel. Dans mon cas particulier, j’ai une machine virtuelle qui charge certaines données lorsqu’une commande est appelée et son test correspondant:

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); } 

Donc, tout cela est assez simple. Cependant, ce que j’aimerais vraiment faire, c’est charger les données à l’aide d’un BackgroundWorker et afficher un message de «chargement» à l’écran. Je change donc la VM en:

 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(); } 

Cela fonctionne bien visuellement au moment de l’exécution, mais mon test échoue maintenant car la propriété n’est pas chargée immédiatement. Quelqu’un peut-il suggérer un bon moyen de tester ce type de chargement? Je suppose que ce dont j’ai besoin est quelque chose comme:

 [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."); } 

Cependant, cela semble vraiment maladroit … Cela fonctionne, mais on se sent juste sale. De meilleures suggestions?


Solution éventuelle :

Basé sur la réponse de David Hall, pour simuler un BackgroundWorker, j’ai fini par faire ce wrapper assez simple autour de BackgroundWorker qui définit deux classes, une qui charge des données de manière asynchrone et une qui se charge de manière synchrone.

  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)); } } } 

Ainsi, dans ma configuration Unity, je peux utiliser SyncWorker pour les tests et AsyncWorker pour la production. Mon ViewModel devient alors:

 public class MyViewModel(IWorker bgWorker) { public void OnLoadData() { IsBusy = true; bgWorker.Run( (sender, e) => { MyData = wcfClient.LoadData(); }, (sender, e) => { IsBusy = false; }); } } 

Notez que la chose que j’ai marquée comme étant wcfClient est en fait un simulacre dans mes tests aussi, donc après l’appel à vm.LoadDataCommand.Execute() je peux également valider que wcfClient.LoadData() été appelé.

Introduisez un travailleur d’arrière-plan factice qui vérifie que vous l’appelez correctement mais qui revient immédiatement avec une réponse prédéfinie.

Changez votre modèle de vue pour permettre l’dependency injections, soit par injection de propriété, soit par injection de constructeur (je montre l’injection de constructeur ci-dessous), puis lors du test, vous passez dans le faux arrière-plan. Dans le monde réel, vous insérez la mise en œuvre réelle lors de la création de la machine virtuelle.

 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(); } } 

En fonction de votre structure (Unity / Prism dans votre cas), connecter le bon travailleur d’arrière-plan ne devrait pas être trop difficile à faire.

Le seul problème de cette approche est que la plupart des classes Microsoft, y compris BackGroundWorker, n’implémentent pas les interfaces. Il peut donc être difficile de les imiter / se moquer.

La meilleure approche que j’ai trouvée consiste à créer votre propre interface pour l’object à simuler, puis un object encapsuleur placé au-dessus de la classe Microsoft actuelle. Pas idéal, car vous avez une mince couche de code non testé, mais au moins, cela signifie que la surface non testée de votre application passe dans des frameworks de test et s’éloigne du code de l’application.

Vous pouvez éviter cette abstraction supplémentaire si vous êtes prêt à l’échanger contre une petite quantité de pollution du modèle de vue (c’est-à-dire l’introduction d’un code utilisé uniquement pour les besoins de vos tests) comme suit:

Tout d’abord, ajoutez un AutoResetEvent (ou ManualResetEvent) facultatif à votre constructeur de modèle de vue et assurez-vous de “définir” cette instance AutoResetEvent lorsque votre travailleur d’arrière-plan termine le gestionnaire “RunWorkerCompleted”.

 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(); } } 

Vous pouvez maintenant passer une instance dans le cadre de votre test unitaire.

 [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 } 

C’est précisément pour cela que les classes AutoResetEvent (et ManualResetEvent) ont été conçues. Donc, mis à part la pollution du code de modèle de vue légère, je pense que cette solution est très soignée.