2009-07-09 18 views
45

Tengo problemas para lograr que Dispatcher ejecute un delegado que le paso al probar la unidad. Todo funciona bien cuando estoy ejecutando el programa, pero, durante una prueba de unidad el siguiente código no funcionará:Uso de WPF Dispatcher en pruebas unitarias

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 para conseguir un modelo de vista Dispatcher:

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

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

+0

¿Qué error recibes? –

+0

No obtengo ningún error. Justo lo que se pasa a BeginInvoke en el Dispatcher nunca se ejecuta. –

+1

Seré honesto y diré que no he tenido que probar un modelo de vista que aún utiliza un despachador. ¿Es posible que el despachador no se esté ejecutando? ¿Llamaría a Dispatcher.CurrentDispatcher.Run() en la ayuda de prueba? Tengo curiosidad, así que publica los resultados si los obtienes. –

Respuesta

82

Al usar el Marco de prueba de unidad de Visual Studio no necesita inicializar el Distribuidor 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# Código:

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

se encuentra esta clase también en el WPF Application Framework (WAF).

+4

Prefiero esta respuesta a la respuesta aceptada, ya que esta solución se puede ejecutar en un caso de prueba escrito de forma secuencial, mientras que la respuesta aceptada requiere que el código de prueba se escriba con un enfoque orientado a la devolución de llamada. –

+0

Brillante. Gracias por compartir. – ozczecho

+3

Esto funcionó de maravilla para mí. Esta es mi respuesta aceptada –

2

Cuando llama a Dispatcher.BeginInvoke, está indicando al asignador que ejecute los delegados en su subproceso cuando el subproceso está inactivo.

Al ejecutar las pruebas unitarias, el hilo principal será nunca estará inactivo. Ejecutará todas las pruebas y luego finalizará.

Para que esta unidad de aspecto se pueda probar, deberá cambiar el diseño subyacente para que no esté utilizando el despachador del hilo principal. Otra alternativa es utilizar el 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, mientras que desconocen el DispatcherFrame. Estoy bastante feliz de haberme equivocado con este: DispatcherFrame ha resultado ser extremadamente útil.

0

Si su objetivo es evitar errores en el acceso DependencyObject s, me sugieren que, en lugar de jugar con los hilos y Dispatcher de forma explícita, se debe asegurar que sus pruebas se ejecutan en un (único) STAThread hilo.

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 apuntar a varias formas de hacer esto:

  • Si utiliza NUnit> = 2.5.0, hay un atributo que puede apuntar [RequiresSTA] métodos de ensayo o clases . 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 versiones anteriores de NUnit, puede configurar NUnit para usar una secuencia de [STAThread] con un archivo de configuración, consulte por ejemplo this blog post de Chris Headgate.
  • Finalmente, the same blog post tiene un método alternativo (que he utilizado con éxito en el pasado) para crear su propio subproceso [STAThread] para ejecutar su prueba.
15

Puede probar la unidad utilizando un despachador, solo tiene que utilizar 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<IDomainObject>(); 
DomainCollection sut = new DomainCollection(dispatcher, domainCollectionMetaData, parentDomainObject); 

IDomainObject domainObject = MockRepository.GenerateMock<IDomainObject>(); 

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é here.

+0

Sí, acabo de regresar para actualizar esta pregunta con la forma en que lo hice al final. ¡Leí la misma publicación, creo! –

2

Creación de una 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); 
} 
20

Hemos resuelto este problema mediante la simple burla a cabo el despachador detrás de una interfaz, y tirando en la interfaz de nuestro contenedor COI. Esta es la unión:

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

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

[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í está una maqueta que suministramos al código durante las pruebas de unidad:

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

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

+0

cómo simular el método DispatcherInvoke? – lukaszk

+0

@lukaszk, dependiendo de su marco de burla, configuraría el método Invoke en su simulacro para ejecutar realmente el delegado pasado a él (si ese fuera el comportamiento que necesita). No es necesario que ejecute ese delegado, tengo algunas pruebas en las que solo verifico que el delegado correcto se pasó al simulacro. –

2

Si desea aplicar la lógica en jbe's answer 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 utilizar con el despachador actual:

Dispatcher.CurrentDispatcher.PumpUntilDry(); 

prefiero esta variación, ya que puede ser utilizado en más situaciones, se implementa utilizando menos código, y tiene una sintaxis más intuitiva. Para obtener información adicional sobre DispatcherFrame, consulte excellent blog writeup.

+1

ese es un nombre de método extraño ... –

0

Estoy usando la tecnología MSTest y Windows Forms con el paradigma MVVM. Después de probar muchas soluciones Por último, esta (found on Vincent Grondin blog) 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 utilizarlo 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)); 
    } 
1

He resuelto este problema mediante la creación de una nueva aplicación en mi configuración de prueba de unidad.

Luego, 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é el AssemblyInitialize y lo puse en su propia clase ApplicationInitializer.

[TestClass] 
public class ApplicationInitializer 
{ 
    [AssemblyInitialize] 
    public static void AssemblyInitialize(TestContext context) 
    { 
     var waitForApplicationRun = new TaskCompletionSource<bool>() 
     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 
    } 
} 
0

Yo sugiero agregar un método más a la DispatcherUtil llaman DoEventsSync() y llame a la 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 publicar esto como otra respuesta no sólo 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; 
     } 
    } 
0

he logrado esto, envolviendo Dispatcher en mi propia interfaz IDispatcher, y luego usando Moq para verificar la llamada a la que era hecho. Interfaz

IDispatcher:

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

real aplicación 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); 
    } 
} 

Inicialización despachador en su clase bajo prueba:

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

que imita el despachador dentro de las pruebas de unidad (en este caso mi controlador de eventos es OnMyEventHandler y acepta un solo parámetro bool ca lled myBoolParameter)

[Test] 
public void When_DoSomething_Then_InvokeMyEventHandler() 
{ 
    var dispatcher = new Mock<IDispatcher>(); 

    ClassUnderTest classUnderTest = new ClassUnderTest(dispatcher.Object); 

    Action<bool> 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); 
} 
0

¿Qué tal 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(); 
    }