Usar el despachador de WPF en pruebas unitarias

Tengo problemas para que Dispatcher ejecute un delegado al que le paso cuando prueba la unidad. Todo funciona bien cuando estoy ejecutando el progtwig, pero, durante una prueba unitaria, no se ejecutará el siguiente código:

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

Tengo este código en mi clase base viewmodel para obtener un Dispatcher:

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

¿Hay algo que deba hacer para inicializar Dispatcher para pruebas unitarias? El despachador nunca ejecuta el código en el delegado.

Al usar el Marco de prueba de unidad de Visual Studio, no necesita inicializar el despachador usted mismo. Tiene toda la razón, que Dispatcher no procesa automáticamente su cola.

Puede escribir un método simple de ayuda “DispatcherUtil.DoEvents ()” que le dice al despachador que procese su cola.

C # Code:

 public static class DispatcherUtil { [SecurityPermissionAttribute(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; } } 

También encuentra esta clase en el Marco de Aplicación de WPF (WAF) .

Hemos resuelto este problema simplemente burlándonos del despachador detrás de una interfaz, y tirando de la interfaz de nuestro contenedor IOC. Aquí está la interfaz:

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

Aquí está la implementación concreta registrada en el contenedor de IOC para la aplicación real

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

Y aquí hay un simulacro que proporcionamos al código durante las pruebas unitarias:

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

También tenemos una variante de MockDispatcher que ejecuta delegates en un hilo de fondo, pero no es necesario la mayor parte del tiempo

Puede probar la unidad usando un despachador, solo necesita usar el DispatcherFrame. Aquí hay un ejemplo de una de las pruebas de mi unidad que usa el DispatcherFrame para forzar la ejecución de la cola del despachador.

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

Me enteré aquí .

Cuando llama a Dispatcher.BeginInvoke, le indica al despachador que ejecute los delegates en su subproceso cuando el subproceso está inactivo .

Cuando se ejecutan pruebas unitarias, el hilo principal nunca estará inactivo. Ejecutará todas las pruebas y luego finalizará.

Para que esta unidad de aspecto sea comprobable, deberá cambiar el diseño subyacente para que no utilice el despachador del hilo principal. Otra alternativa es utilizar System.ComponentModel.BackgroundWorker para modificar los usuarios en un hilo diferente. (Esto es solo un ejemplo, podría ser inapropiado según el contexto).


Editar (5 meses después) Escribí esta respuesta sin tener conocimiento del DispatcherFrame. Estoy bastante feliz de haberme equivocado con este: DispatcherFrame ha resultado ser extremadamente útil.

Crear un DipatcherFrame funcionó muy bien para mí:

 [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 desea aplicar la lógica en la respuesta de jbe a cualquier despachador (no solo Dispatcher.CurrentDispatcher , puede usar el siguiente método de extensión).

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

Uso:

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

Para usar con el despachador actual:

 Dispatcher.CurrentDispatcher.PumpUntilDry(); 

Prefiero esta variación porque puede usarse en más situaciones, se implementa usando menos código y tiene una syntax más intuitiva.

Para obtener información adicional sobre DispatcherFrame , consulte este excelente artículo de blog .

Resolví este problema creando una nueva aplicación en la configuración de prueba de mi unidad.

Entonces, cualquier clase bajo prueba que acceda a Application.Current.Dispatcher encontrará un despachador.

Como solo se permite una aplicación en un dominio de aplicación, utilicé AssemblyInitialize y lo puse en su propia clase 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 su objective es evitar errores al acceder a DependencyObject s, le sugiero que, en lugar de jugar con hilos y Dispatcher explícitamente, simplemente se asegure de que sus pruebas se ejecuten en un único hilo STAThread .

Esto puede o no satisfacer sus necesidades, para mí al menos siempre ha sido suficiente para probar cualquier cosa relacionada con DependencyObject / WPF.

Si desea probar esto, puedo indicarle varias maneras de hacerlo:

  • Si usa NUnit> = 2.5.0, existe un atributo [RequiresSTA] que puede orientar los métodos o clases de prueba. Sin embargo, ten cuidado si utilizas un corredor de prueba integrado, como por ejemplo, el corredor R # 4.5 NUnit parece estar basado en una versión anterior de NUnit y no puede usar este atributo.
  • Con las versiones anteriores de NUnit, puede configurar NUnit para usar un hilo [STAThread] con un archivo de configuración; consulte, por ejemplo, esta publicación de blog de Chris Headgate.
  • Finalmente, la misma publicación de blog tiene un método alternativo (que he utilizado con éxito en el pasado) para crear tu propio hilo [STAThread] para ejecutar tu prueba.

Estoy usando la MSTest y Windows Forms con el paradigma MVVM. Después de probar muchas soluciones, finalmente esto (que se encuentra en el blog de Vincent Grondin) funciona para mí:

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

Y úsalo como:

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

Sugiero agregar un método más a DispatcherUtil, llámelo DoEventsSync () y simplemente llame al Dispatcher para invocar en lugar de BeginInvoke. Esto es necesario si realmente tiene que esperar hasta que el Dispatcher haya procesado todos los marcos. Estoy publicando esto como otra Respuesta, no solo como un comentario, ya que toda la clase es larga:

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

Lo logré envolviendo Dispatcher en mi propia interfaz de IDispatcher y luego usando Moq para verificar que se realizó la llamada.

Interfaz IDispatcher:

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

Implementación real del despachador:

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

Inicializando el despachador en su clase bajo prueba:

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

Mocking del despachador dentro de las pruebas unitarias (en este caso, mi controlador de eventos es OnMyEventHandler y acepta un solo parámetro bool llamado 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); } 

¿Qué hay de ejecutar la prueba en un hilo dedicado con soporte 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(); } 

Llego tarde, pero así es como lo hago:

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