2009-07-09 4 views
45

Non riesco a far eseguire al Dispatcher un delegato che sto passando ad esso durante il test dell'unità. Tutto funziona bene quando sono in esecuzione il programma, ma, durante una prova di unità il seguente codice non verrà eseguito:Utilizzo del Dispatcher WPF in unit test

this.Dispatcher.BeginInvoke(new ThreadStart(delegate 
{ 
    this.Users.Clear(); 

    foreach (User user in e.Results) 
    { 
     this.Users.Add(user); 
    } 
}), DispatcherPriority.Normal, null); 

Ho questo codice nella mia classe di base ViewModel per ottenere un Dispatcher:

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

C'è qualcosa che devo fare per inizializzare il Dispatcher per i test di unità? Il Dispatcher non esegue mai il codice nel delegato.

+0

Che errore ricevi? –

+0

Non ho alcun errore. Solo ciò che viene passato a BeginInvoke sul Dispatcher non viene mai eseguito. –

+1

Sarò onesto e dirò che non ho dovuto testare un modello di vista che utilizza ancora un dispatcher. È possibile che il dispatcher non stia funzionando. Chiamerebbe Dispatcher.CurrentDispatcher.Run() nella tua guida di prova? Sono curioso, quindi pubblica i risultati se li ottieni. –

risposta

82

Utilizzando il quadro di test dell'unità di Visual Studio non è necessario inizializzare il Dispatcher. Hai assolutamente ragione, che il Dispatcher non elabora automaticamente la coda.

È possibile scrivere un semplice metodo di supporto "DispatcherUtil.DoEvents()" che indica al Dispatcher di elaborare la coda.

C# Codice:

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

a trovare questa classe anche nel WPF Application Framework (WAF).

+4

Preferisco questa risposta alla risposta accettata, poiché questa soluzione può essere eseguita in un caso di test creato in sequenza, mentre la risposta accettata richiede che il codice di prova sia scritto in un approccio orientato alla callback. –

+0

Brillante. Grazie per la condivisione. – ozczecho

+3

Questo ha funzionato perfettamente per me. Questa è la mia risposta accettata –

2

Quando si chiama Dispatcher.BeginInvoke, si sta istruendo il dispatcher per eseguire i delegati sul thread quando il thread è inattivo.

Quando esegue test di unità, il filo conduttore sarà mai essere inattivo. Eseguirà tutti i test e terminerà.

Per rendere testabile questa unità aspetto, è necessario modificare il progetto sottostante in modo che non utilizzi il dispatcher del thread principale. Un'altra alternativa è quella di utilizzare System.ComponentModel.BackgroundWorker per modificare gli utenti su un thread diverso. (Questo è solo un esempio, potrebbe non essere appropriato a seconda del contesto).


Modifica (5 mesi più tardi) Ho scritto questa risposta, mentre ignaro della DispatcherFrame. Sono abbastanza contento di aver sbagliato su questo - DispatcherFrame si è rivelato estremamente utile.

0

Se il vostro obiettivo è quello di evitare errori durante l'accesso DependencyObject s, mi suggeriscono che, piuttosto che giocare con fili e Dispatcher in modo esplicito, è sufficiente assicurarsi che i test eseguiti in un (singolo) STAThread thread.

Questo potrebbe non essere adatto alle proprie esigenze, almeno per me è sempre stato sufficiente per testare qualsiasi cosa correlata a DependencyObject/WPF.

Se volete provare questo, posso puntare a diversi modi per farlo:

  • Se si utilizza NUnit> = 2.5.0, v'è un attributo [RequiresSTA] che può avere come bersaglio i metodi di prova o classi . Attenzione però se utilizzi un test runner integrato, come ad esempio il runner NUnit R # 4.5 sembra essere basato su una versione precedente di NUnit e non può usare questo attributo.
  • Con le versioni NUnit precedenti, è possibile impostare NUnit per utilizzare un thread [STAThread] con un file di configurazione, vedere ad esempio this blog post di Chris Headgate.
  • Infine, the same blog post ha un metodo di fallback (che ho utilizzato con successo in passato) per creare il proprio thread [STAThread] per eseguire il test.
15

È possibile testare l'unità utilizzando un dispatcher, è sufficiente utilizzare DispatcherFrame. Ecco un esempio di uno dei miei test unitari che utilizza DispatcherFrame per forzare l'esecuzione della coda del dispatcher.

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

L'ho scoperto su di esso here.

+0

Sì, sono appena tornato per aggiornare questa domanda con come l'ho fatto alla fine. Ho letto lo stesso post che penso! –

2

Creazione di un DipatcherFrame lavorato grande per me:

[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

Abbiamo risolto questo problema semplicemente beffardo il dispatcher dietro un'interfaccia, e tirando nell'interfaccia dal nostro contenitore CIO. Ecco l'interfaccia:

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

Ecco l'implementazione concreta registrato nel contenitore del CIO per l'applicazione reale

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

Ed ecco uno finto che forniamo al codice durante il test di unità:

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

Abbiamo anche una variante di MockDispatcher che esegue i delegati in un thread in background, ma non è necessaria la maggior parte del tempo

+0

come imitare il metodo DispatcherInvoke? – lukaszk

+0

@lukaszk, a seconda del tuo framework di simulazione, dovresti impostare il metodo Invoke sul tuo mock per eseguire effettivamente il delegato passato (se questo fosse il comportamento che ti occorre). Non è necessariamente necessario eseguire quel delegato, ho alcuni test in cui ho appena verificato che il delegato corretto è stato passato al mock. –

2

Se si desidera applicare la logica in jbe's answer a qualsiasi dispatcher (non solo Dispatcher.CurrentDispatcher, è possibile utilizzare il seguente metodo di estensione.

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

Per utilizzare con il dispatcher corrente:

Dispatcher.CurrentDispatcher.PumpUntilDry(); 

preferisco questa variante, perché può essere utilizzato in più situazioni, viene implementata utilizzando meno codice, e ha una sintassi più intuitiva.

Per ulteriori informazioni sullo DispatcherFrame, consultare questo excellent blog writeup.

+1

questo è un nome strano per il metodo .. –

0

Sto usando la tecnologia MSTest e Windows Forms con il paradigma MVVM. Dopo aver provato molte soluzioni Infine, questa (found on Vincent Grondin blog) opere per me:

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

e usarlo come:

[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

Ho risolto questo problema con la creazione di una nuova applicazione nella mia messa a punto test di unità.

Quindi qualsiasi classe in prova che accede a Application.Current.Dispatcher troverà un dispatcher.

Poiché in AppDomain è consentita solo un'applicazione, ho utilizzato AssemblyInitialize e l'ho inserito nella classe 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

Suggerisco di aggiungere un metodo più al DispatcherUtil chiamano DoEventsSync() e basta chiamare il Dispatcher di invocare al posto di BeginInvoke. Questo è necessario se devi davvero aspettare fino a quando il Dispatcher ha elaborato tutti i frame. Sto inviando questo come un'altra risposta non solo un commento, dal momento che tutta la classe è lungo:

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

Ho compiuto questo avvolgendo Dispatcher nella mia propria interfaccia IDispatcher, e quindi utilizzando Moq per verificare la chiamata ad esso era fatto. Interfaccia

IDispatcher:

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

implementazione dispatcher reale:

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

inizializzazione dispatcher nella classe in prova:

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

Mocking il dispatcher all'interno test di unità (in questo caso il mio gestore di eventi è OnMyEventHandler e accetta un singolo parametro 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

Come eseguire il test su un thread dedicato con il supporto di 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(); 
    }