Utilisation du répartiteur WPF dans les tests unitaires

J’ai du mal à faire en sorte que le Dispatcher exécute un délégué que je lui passe lors des tests unitaires. Tout fonctionne bien lorsque j’exécute le programme, mais lors d’un test unitaire, le code suivant ne fonctionnera pas:

this.Dispatcher.BeginInvoke(new ThreadStart(delegate { this.Users.Clear(); foreach (User user in e.Results) { this.Users.Add(user); } }), DispatcherPriority.Normal, null); 

J’ai ce code dans ma classe de base viewmodel pour obtenir un Dispatcher:

 if (Application.Current != null) { this.Dispatcher = Application.Current.Dispatcher; } else { this.Dispatcher = Dispatcher.CurrentDispatcher; } 

Dois-je faire quelque chose pour initialiser le Dispatcher pour les tests unitaires? Le répartiteur n’exécute jamais le code dans le délégué.

En utilisant Visual Studio Unit Test Framework, vous n’avez pas besoin d’initialiser le répartiteur vous-même. Vous avez absolument raison, le Dispatcher ne traite pas automatiquement sa queue.

Vous pouvez écrire une méthode d’assistance simple «DispatcherUtil.DoEvents ()» qui indique au Dispatcher de traiter sa queue.

Code C #:

 public static class DispatcherUtil { [SecurityPermissionAtsortingbute(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)] public static void DoEvents() { DispatcherFrame frame = new DispatcherFrame(); Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(ExitFrame), frame); Dispatcher.PushFrame(frame); } private static object ExitFrame(object frame) { ((DispatcherFrame)frame).Continue = false; return null; } } 

Vous trouvez également cette classe dans WPF Application Framework (WAF) .

Nous avons résolu ce problème en éliminant simplement le répartiteur derrière une interface et en tirant l’interface de notre conteneur IOC. Voici l’interface:

 public interface IDispatcher { void Dispatch( Delegate method, params object[] args ); } 

Voici l’implémentation concrète enregistrée dans le conteneur IOC pour l’application réelle

 [Export(typeof(IDispatcher))] public class ApplicationDispatcher : IDispatcher { public void Dispatch( Delegate method, params object[] args ) { UnderlyingDispatcher.BeginInvoke(method, args); } // ----- Dispatcher UnderlyingDispatcher { get { if( App.Current == null ) throw new InvalidOperationException("You must call this method from within a running WPF application!"); if( App.Current.Dispatcher == null ) throw new InvalidOperationException("You must call this method from within a running WPF application with an active dispatcher!"); return App.Current.Dispatcher; } } } 

Et voici un simulacre que nous fournissons au code lors des tests unitaires:

 public class MockDispatcher : IDispatcher { public void Dispatch(Delegate method, params object[] args) { method.DynamicInvoke(args); } } 

Nous avons également une variante du MockDispatcher qui exécute les delegates dans un thread d’arrière-plan, mais ce n’est pas nécessaire la plupart du temps

Vous pouvez tester les unités à l’aide d’un répartiteur, il vous suffit d’utiliser le DispatcherFrame. Voici un exemple de l’un de mes tests unitaires qui utilise DispatcherFrame pour forcer l’exécution de la queue du répartiteur.

 [TestMethod] public void DomainCollection_AddDomainObjectFromWorkerThread() { Dispatcher dispatcher = Dispatcher.CurrentDispatcher; DispatcherFrame frame = new DispatcherFrame(); IDomainCollectionMetaData domainCollectionMetaData = this.GenerateIDomainCollectionMetaData(); IDomainObject parentDomainObject = MockRepository.GenerateMock(); DomainCollection sut = new DomainCollection(dispatcher, domainCollectionMetaData, parentDomainObject); IDomainObject domainObject = MockRepository.GenerateMock(); sut.SetAsLoaded(); bool raisedCollectionChanged = false; sut.ObservableCollection.CollectionChanged += delegate(object sender, NotifyCollectionChangedEventArgs e) { raisedCollectionChanged = true; Assert.IsTrue(e.Action == NotifyCollectionChangedAction.Add, "The action was not add."); Assert.IsTrue(e.NewStartingIndex == 0, "NewStartingIndex was not 0."); Assert.IsTrue(e.NewItems[0] == domainObject, "NewItems not include added domain object."); Assert.IsTrue(e.OldItems == null, "OldItems was not null."); Assert.IsTrue(e.OldStartingIndex == -1, "OldStartingIndex was not -1."); frame.Continue = false; }; WorkerDelegate worker = new WorkerDelegate(delegate(DomainCollection domainCollection) { domainCollection.Add(domainObject); }); IAsyncResult ar = worker.BeginInvoke(sut, null, null); worker.EndInvoke(ar); Dispatcher.PushFrame(frame); Assert.IsTrue(raisedCollectionChanged, "CollectionChanged event not raised."); } 

Je l’ai découvert ici .

Lorsque vous appelez Dispatcher.BeginInvoke, vous demandez au répartiteur d’exécuter les delegates sur son thread lorsque le thread est inactif .

Lors de l’exécution des tests unitaires, le thread principal ne sera jamais inactif. Il exécutera tous les tests puis se terminera.

Pour que cet aspect puisse être testé, vous devrez modifier la conception sous-jacente afin qu’elle n’utilise pas le répartiteur du thread principal. Une autre alternative consiste à utiliser System.ComponentModel.BackgroundWorker pour modifier les utilisateurs sur un thread différent. (Ceci n’est qu’un exemple, cela peut être approprié selon le contexte).


Edit (5 mois plus tard) J’ai écrit cette réponse sans savoir que le DispatcherFrame. Je suis très content d’avoir eu tort sur celui-ci – DispatcherFrame s’est avéré extrêmement utile.

Créer un DipatcherFrame a été très utile pour moi:

 [TestMethod] public void Search_for_item_returns_one_result() { var searchService = CreateSearchServiceWithExpectedResults("test", 1); var eventAggregator = new SimpleEventAggregator(); var searchViewModel = new SearchViewModel(searchService, 10, eventAggregator) { SearchText = searchText }; var signal = new AutoResetEvent(false); var frame = new DispatcherFrame(); // set the event to signal the frame eventAggregator.Subscribe(new ProgressCompleteEvent(), () => { signal.Set(); frame.Continue = false; }); searchViewModel.Search(); // dispatcher call happening here Dispatcher.PushFrame(frame); signal.WaitOne(); Assert.AreEqual(1, searchViewModel.TotalFound); } 

Si vous souhaitez appliquer la logique dans la réponse de jbe à un répartiteur (et pas seulement à Dispatcher.CurrentDispatcher , vous pouvez utiliser la méthode d’extension suivante).

 public static class DispatcherExtentions { public static void PumpUntilDry(this Dispatcher dispatcher) { DispatcherFrame frame = new DispatcherFrame(); dispatcher.BeginInvoke( new Action(() => frame.Continue = false), DispatcherPriority.Background); Dispatcher.PushFrame(frame); } } 

Usage:

 Dispatcher d = getADispatcher(); d.PumpUntilDry(); 

A utiliser avec le répartiteur actuel:

 Dispatcher.CurrentDispatcher.PumpUntilDry(); 

Je préfère cette variante car elle peut être utilisée dans plus de situations, est implémentée avec moins de code et a une syntaxe plus intuitive.

Pour plus d’informations sur DispatcherFrame , consultez cet excellent blog .

J’ai résolu ce problème en créant une nouvelle application dans ma configuration de test unitaire.

Ensuite, toute classe sous test dont l’access à Application.Current.Dispatcher trouvera un répartiteur.

Étant donné qu’une seule application est autorisée dans un AppDomain, j’ai utilisé AssemblyInitialize et l’ai placée dans sa propre classe ApplicationInitializer.

 [TestClass] public class ApplicationInitializer { [AssemblyInitialize] public static void AssemblyInitialize(TestContext context) { var waitForApplicationRun = new TaskCompletionSource() Task.Run(() => { var application = new Application(); application.Startup += (s, e) => { waitForApplicationRun.SetResult(true); }; application.Run(); }); waitForApplicationRun.Task.Wait(); } [AssemblyCleanup] public static void AssemblyCleanup() { Application.Current.Dispatcher.Invoke(Application.Current.Shutdown); } } [TestClass] public class MyTestClass { [TestMethod] public void MyTestMethod() { // implementation can access Application.Current.Dispatcher } } 

Si votre objective est d’éviter des erreurs lors de l’access à DependencyObject , je suggère plutôt que, plutôt que de jouer explicitement avec des threads et Dispatcher , vous vous assurez simplement que vos tests s’exécutent dans un (unique) thread STAThread .

Cela peut ou non répondre à vos besoins, pour moi au moins, cela a toujours été suffisant pour tester tout ce qui est lié à DependencyObject / WPF.

Si vous souhaitez essayer ceci, je peux vous indiquer plusieurs façons de procéder:

  • Si vous utilisez NUnit> = 2.5.0, il existe un atsortingbut [RequiresSTA] qui peut cibler des méthodes ou des classes de test. Attention cependant, si vous utilisez un runner de test intégré, comme par exemple le runner R # 4.5 NUnit semble être basé sur une version antérieure de NUnit et ne peut pas utiliser cet atsortingbut.
  • Avec les anciennes versions de NUnit, vous pouvez définir NUnit pour utiliser un [STAThread] avec un fichier de configuration, voir par exemple cet article de Chris Headgate.
  • Enfin, le même article de blog a une méthode de remplacement (que j’ai utilisée avec succès par le passé) pour créer votre propre [STAThread] pour exécuter votre test.

J’utilise la technologie MSTest et Windows Forms avec le paradigme MVVM. Après avoir essayé de nombreuses solutions, ceci (trouvé sur le blog de Vincent Grondin) fonctionne pour moi:

  internal Thread CreateDispatcher() { var dispatcherReadyEvent = new ManualResetEvent(false); var dispatcherThread = new Thread(() => { // This is here just to force the dispatcher // infrastructure to be setup on this thread Dispatcher.CurrentDispatcher.BeginInvoke(new Action(() => { })); // Run the dispatcher so it starts processing the message // loop dispatcher dispatcherReadyEvent.Set(); Dispatcher.Run(); }); dispatcherThread.SetApartmentState(ApartmentState.STA); dispatcherThread.IsBackground = true; dispatcherThread.Start(); dispatcherReadyEvent.WaitOne(); SynchronizationContext .SetSynchronizationContext(new DispatcherSynchronizationContext()); return dispatcherThread; } 

Et l’utiliser comme:

  [TestMethod] public void Foo() { Dispatcher .FromThread(CreateDispatcher()) .Invoke(DispatcherPriority.Background, new DispatcherDelegate(() => { _barViewModel.Command.Executed += (sender, args) => _done.Set(); _barViewModel.Command.DoExecute(); })); Assert.IsTrue(_done.WaitOne(WAIT_TIME)); } 

Je suggère d’append une méthode supplémentaire à DispatcherUtil pour l’appeler DoEventsSync () et d’appeler simplement le Dispatcher à Invoke au lieu de BeginInvoke. Cela est nécessaire si vous devez vraiment attendre que le Dispatcher ait traité toutes les images. Je publie ceci comme une autre réponse, pas simplement un commentaire, puisque toute la classe est longue:

  public static class DispatcherUtil { [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)] public static void DoEvents() { var frame = new DispatcherFrame(); Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(ExitFrame), frame); Dispatcher.PushFrame(frame); } public static void DoEventsSync() { var frame = new DispatcherFrame(); Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Background, new DispatcherOperationCallback(ExitFrame), frame); Dispatcher.PushFrame(frame); } private static object ExitFrame(object frame) { ((DispatcherFrame)frame).Continue = false; return null; } } 

J’ai accompli ceci en encapsulant Dispatcher dans ma propre interface IDispatcher, puis en utilisant Moq pour vérifier que l’appel a été effectué.

Interface IDispatcher:

 public interface IDispatcher { void BeginInvoke(Delegate action, params object[] args); } 

Implémentation réelle du répartiteur:

 class RealDispatcher : IDispatcher { private readonly Dispatcher _dispatcher; public RealDispatcher(Dispatcher dispatcher) { _dispatcher = dispatcher; } public void BeginInvoke(Delegate method, params object[] args) { _dispatcher.BeginInvoke(method, args); } } 

Initialisation du répartiteur dans votre classe sous test:

 public ClassUnderTest(IDispatcher dispatcher = null) { _dispatcher = dispatcher ?? new UiDispatcher(Application.Current?.Dispatcher); } 

Se moquer du répartiteur à l’intérieur des tests unitaires (dans ce cas, mon gestionnaire d’événement est OnMyEventHandler et accepte un seul paramètre bool appelé myBoolParameter)

 [Test] public void When_DoSomething_Then_InvokeMyEventHandler() { var dispatcher = new Mock(); ClassUnderTest classUnderTest = new ClassUnderTest(dispatcher.Object); Action OnMyEventHanlder = delegate (bool myBoolParameter) { }; classUnderTest.OnMyEvent += OnMyEventHanlder; classUnderTest.DoSomething(); //verify that OnMyEventHandler is invoked with 'false' argument passed in dispatcher.Verify(p => p.BeginInvoke(OnMyEventHanlder, false), Times.Once); } 

Que diriez-vous d’exécuter le test sur un thread dédié avec le support de Dispatcher?

  void RunTestWithDispatcher(Action testAction) { var thread = new Thread(() => { var operation = Dispatcher.CurrentDispatcher.BeginInvoke(testAction); operation.Completed += (s, e) => { // Dispatcher finishes queued tasks before shuts down at idle priority (important for TransientEventTest) Dispatcher.CurrentDispatcher.BeginInvokeShutdown(DispatcherPriority.ApplicationIdle); }; Dispatcher.Run(); }); thread.IsBackground = true; thread.TrySetApartmentState(ApartmentState.STA); thread.Start(); thread.Join(); } 

Je suis en retard mais c’est comme ça que je le fais:

 public static void RunMessageLoop(Func action) { var originalContext = SynchronizationContext.Current; Exception exception = null; try { SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext()); action.Invoke().ContinueWith(t => { exception = t.Exception; }, TaskContinuationOptions.OnlyOnFaulted).ContinueWith(t => Dispatcher.ExitAllFrames(), TaskScheduler.FromCurrentSynchronizationContext()); Dispatcher.Run(); } finally { SynchronizationContext.SetSynchronizationContext(originalContext); } if (exception != null) throw exception; }