2010-03-03 12 views
13

Ho una classe, che si iscrive a un evento tramite l'aggregatore di eventi PRISM.Come posso testare le sottoscrizioni di aggregatori di eventi Prism, su UIThread?

Poiché è un po 'difficile prendere in giro l'aggregatore di eventi come indicato in here, ho appena creato un'istanza reale e lo ho passato al sistema sottoposto a test.

Nel mio test pubblichiamo quindi l'evento tramite quell'aggregatore e poi controllo come reagisce il mio sistema in prova. Poiché l'evento verrà generato da FileSystemWatcher durante la produzione, desidero utilizzare l'invio automatico sottoscrivendo l'UIThread, in modo da poter aggiornare l'interfaccia utente una volta generato l'evento.

Il problema è che durante il test l'evento non viene mai notato nel sistema in prova a meno che non mi iscriva all'UTIThread.

Sto usando MSpec per i miei test, che eseguo da VS2008 tramite TDD.Net. Aggiunta [RequiresSta] alla mia classe di test non ha aiutato

Qualcuno ha una soluzione, che mi salva dal cambiare il ThreadOption durante i miei test (ad esempio tramite una proprietà - quello che un brutto hack) ???

risposta

16

Se deridere sia l'evento e l'aggregatore eventi, e utilizzare richiamata del moq, si può fare.

Ecco un esempio:

Mock<IEventAggregator> mockEventAggregator; 
Mock<MyEvent> mockEvent; 

mockEventAggregator.Setup(e => e.GetEvent<MyEvent>()).Returns(mockEvent.Object); 

// Get a copy of the callback so we can "Publish" the data 
Action<MyEventArgs> callback = null; 

mockEvent.Setup(
    p => 
    p.Subscribe(
     It.IsAny<Action<MyEventArgs>>(), 
     It.IsAny<ThreadOption>(), 
     It.IsAny<bool>(), 
     It.IsAny<Predicate<MyEventArgs>>())) 
     .Callback<Action<MyEventArgs>, ThreadOption, bool, Predicate<MyEventArgs>>(
     (e, t, b, a) => callback = e); 


// Do what you need to do to get it to subscribe 

// Callback should now contain the callback to your event handler 
// Which will allow you to invoke the callback on the test's thread 
// instead of the UI thread 
callback.Invoke(new MyEventArgs(someObject)); 

// Assert 
+1

Com'è che hai risposto a questo 2 anni dopo la mia risposta con la stessa soluzione e ne ottieni il merito? :) –

+0

Hai aggiornato la tua risposta? Non ricordavo di aver visto l'EA deriso nella tua risposta ... – TTat

+1

È la prima riga della mia risposta 'Mock eventAggregatorMock = new Mock ();' –

15

Penso davvero che dovresti usare i mock per tutto e non l'EventAggregator. Non è affatto difficile prendere in giro ... Non penso che la risposta collegata provi molto sulla verificabilità di EventAggregator.

Ecco il test. Non uso MSpec, ma ecco il test in Moq. Non hai fornito alcun codice, quindi lo sto basando sul codice linkato. Il tuo scenario è un po 'più difficile dello scenario collegato perché l'altro OP voleva solo sapere come verificare che fosse stato chiamato l'abbonamento, ma in realtà vuoi chiamare il metodo che è stato passato nell'iscrive ... qualcosa di più difficile, ma non molto.

//Arrange! 
Mock<IEventAggregator> eventAggregatorMock = new Mock<IEventAggregator>(); 
Mock<PlantTreeNodeSelectedEvent> eventBeingListenedTo = new Mock<PlantTreeNodeSelectedEvent>(); 

Action<int> theActionPassed = null; 
//When the Subscribe method is called, we are taking the passed in value 
//And saving it to the local variable theActionPassed so we can call it. 
eventBeingListenedTo.Setup(theEvent => theEvent.Subscribe(It.IsAny<Action<int>>())) 
        .Callback<Action<int>>(action => theActionPassed = action); 

eventAggregatorMock.Setup(e => e.GetEvent<PlantTreeNodeSelectedEvent>()) 
        .Returns(eventBeingListenedTo.Object); 

//Initialize the controller to be tested. 
PlantTreeController controllerToTest = new PlantTreeController(eventAggregatorMock.Object); 

//Act! 
theActionPassed(3); 

//Assert! 
Assert.IsTrue(controllerToTest.MyValue == 3); 
+0

Grazie Anderson. Aggiornerò il mio test con il tuo suggerimento quando arrivo ad esso. Hai ragione, prendere in giro tutto è la soluzione migliore, faccio delle eccezioni solo quando si tratta di qualcosa che si suppone funzioni, come un componente framework (ad esempio un aggregatore di eventi) e non è troppo pesante sulle risorse e/o lento. –

4

Non è possibile, come questo in quanto può coinvolgere ciò che si sente è un "brutto hack", ma la mia preferenza è quella di utilizzare un vero e proprio EventAggregator piuttosto che beffardo tutto. Pur essendo apparentemente una risorsa esterna, EventAggregator viene eseguito in memoria e quindi non richiede molto set-up, clear down, e non è un collo di bottiglia come altre risorse esterne come database, servizi web, eccetera, e quindi lo sento è appropriato da usare in un test unitario. Su questa base ho usato questo metodo per superare il problema del thread dell'interfaccia utente in NUnit con modifiche minime o rischio al mio codice di produzione per il bene dei test.

In primo luogo ho creato un metodo di estensione in questo modo:

public static class ThreadingExtensions 
{ 
    private static ThreadOption? _uiOverride; 

    public static ThreadOption UiOverride 
    { 
     set { _uiOverride = value; } 
    } 

    public static ThreadOption MakeSafe(this ThreadOption option) 
    { 
     if (option == ThreadOption.UIThread && _uiOverride != null) 
      return (ThreadOption) _uiOverride; 

     return option; 
    } 

}

Poi, in tutte le mie sottoscrizioni di eventi Io uso il seguente:

EventAggregator.GetEvent<MyEvent>().Subscribe 
(
    x => // do stuff, 
    ThreadOption.UiThread.MakeSafe() 
); 

Nel codice di produzione, questo solo funziona perfettamente.A scopo di verifica, tutto quello che devo fare è aggiungere questo nel mio set-up, con un po 'di codice di sincronizzazione nel mio test:

[TestFixture] 
public class ExampleTest 
{ 
    [SetUp] 
    public void SetUp() 
    { 
     ThreadingExtensions.UiOverride = ThreadOption.Background; 
    } 

    [Test] 
    public void EventTest() 
    { 
     // This doesn't actually test anything useful. For a real test 
     // use something like a view model which subscribes to the event 
     // and perform your assertion on it after the event is published. 
     string result = null; 
     object locker = new object(); 
     EventAggregator aggregator = new EventAggregator(); 

     // For this example, MyEvent inherits from CompositePresentationEvent<string> 
     MyEvent myEvent = aggregator.GetEvent<MyEvent>(); 

     // Subscribe to the event in the test to cause the monitor to pulse, 
     // releasing the wait when the event actually is raised in the background 
     // thread. 
     aggregator.Subscribe 
     (
      x => 
      { 
       result = x; 
       lock(locker) { Monitor.Pulse(locker); } 
      }, 
      ThreadOption.UIThread.MakeSafe() 
     ); 

     // Publish the event for testing 
     myEvent.Publish("Testing"); 

     // Cause the monitor to wait for a pulse, but time-out after 
     // 1000 millisconds. 
     lock(locker) { Monitor.Wait(locker, 1000); } 

     // Once pulsed (or timed-out) perform your assertions in the real world 
     // your assertions would be against the object your are testing is 
     // subscribed. 
     Assert.That(result, Is.EqualTo("Testing")); 
    } 
} 

Per rendere l'attesa e pulsante più succinta Ho anche aggiunto i seguenti metodi di estensione a ThreadingExtensions:

public static void Wait(this object locker, int millisecondTimeout) 
    { 
     lock (locker) 
     { 
      Monitor.Wait(locker); 
     } 
    } 

    public static void Pulse(this object locker) 
    { 
     lock (locker) 
     { 
      Monitor.Pulse(locker); 
     } 
    } 

Allora posso fare:

// <snip> 
aggregator.Subscribe(x => locker.Pulse(), ThreadOption.UIThread.MakeSafe()); 

myEvent.Publish("Testing"); 

locker.Wait(1000); 
// </snip> 

Anche in questo caso, se la vostra sensibilità significa che si desidera utilizzare prende in giro, andare per esso. Se preferisci usare la cosa reale, funziona.