2010-03-03 12 views
13

Ich habe eine Klasse, die eine Veranstaltung über PRISMs Ereignisaggregator abonniert.Wie kann ich Prism Event Aggregator-Abonnements auf dem UIThread testen?

Da es etwas schwierig ist, den Ereignisaggregator wie angegeben here zu verspotten, instanziiere ich nur einen echten und übergebe es an das zu testende System.

In meinem Test veröffentliche ich dann das Ereignis über diesen Aggregator und überprüfe dann, wie mein System im Test darauf reagiert. Da das Ereignis während der Produktion von einem FileSystemWatcher ausgelöst wird, möchte ich den automatischen Versand nutzen, indem ich auf dem UIThread subskribiert habe, damit ich meine Benutzeroberfläche aktualisieren kann, sobald das Ereignis ausgelöst wird.

Das Problem ist, dass das Ereignis während des Tests nie in dem getesteten System bemerkt wird, es sei denn, ich abonniere nicht auf dem UIThread.

Ich verwende MSpec für meine Tests, die ich innerhalb von VS2008 über TDD.Net laufen lasse. Hinzufügen [RequiresSta] meiner Testklasse hat nicht geholfen

Hat jemand eine Lösung haben, die mich erspart Änderung der ThreadOption während meiner Tests (beispielsweise über eine Eigenschaft - was für ein hässlicher Hack) ???

Antwort

16

Wenn Sie sowohl das Ereignis als auch den Ereignisaggregator vortäuschen und den Rückruf von moq verwenden, können Sie es tun.

Hier ist ein Beispiel:

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

Wie ist es, dass Sie das 2 Jahre nach meiner Antwort mit der gleichen Lösung beantwortet haben und Sie dafür Anerkennung bekommen? :) –

+0

Hast du deine Antwort aktualisiert? Ich konnte mich nicht erinnern, den EA in deiner Antwort verspottet zu haben ... – TTat

+1

Es ist die erste Zeile meiner Antwort 'Mock eventAggregatorMock = neuer Mock ();' –

15

Ich denke wirklich, dass Sie Mocks für alles und nicht den EventAggregator verwenden sollten. Es ist überhaupt nicht schwer, sich zu veräppeln ... Ich denke nicht, dass die verknüpfte Antwort viel über die Testbarkeit des EventAggregators beweist.

Hier ist dein Test. Ich benutze MSpec nicht, aber hier ist der Test in Moq. Du hast keinen Code zur Verfügung gestellt, also stütze ich ihn auf den Linked-to-Code. Ihr Szenario ist ein wenig schwieriger als das verknüpfte Szenario, weil das andere OP nur wissen wollte, wie man überprüft, dass Subscribe aufgerufen wurde, aber Sie möchten tatsächlich die Methode aufrufen, die im Abonnement übergeben wurde ... etwas schwieriger, aber nicht sehr.

+0

Dank Anderson. Ich werde meinen Test mit deinem Vorschlag aktualisieren, wenn ich dazu komme. Sie haben Recht, alles zu verspotten ist die beste Lösung, ich mache nur Ausnahmen, wenn Sie wirklich davon ausgehen, dass es funktioniert, wie eine Framework-Komponente (z. B. Ereignisaggregator) und es ist nicht zu schwer auf Ressourcen und/oder langsam. –

4

Sie können dies nicht so, wie es bedeuten kann, was Sie fühlen, ist ein „hässlicher Hack“, aber meine Präferenz ist eher ein echtes Eventaggregator zu verwenden, als alles verspotten. Obwohl der EventAggregator angeblich eine externe Ressource ist, wird er im Speicher ausgeführt und benötigt daher nicht viel Setup, Cleardown und ist kein Flaschenhals wie andere externe Ressourcen wie Datenbanken, Web-Services und so weiter und daher fühle ich es eignet sich zur Verwendung in einem Komponententest. Auf dieser Grundlage habe ich diese Methode verwendet, um das UI-Thread-Problem in NUnit mit minimalen Änderungen oder Risiken für meinen Produktionscode zu umgehen.

Zum einen habe ich eine Erweiterungsmethode wie folgt:

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

}

Dann in meinem Ereignisabonnements verwende ich die folgende:

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

In Produktionscode, diese nur funktioniert nahtlos.Für Prüfzwecke, alles, was ich tun muß, ist dies mit einem bisschen Synchronisationscode in meinem Test in meinem Set-up hinzufügen:

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

Um die Warte zu machen und pulsierend prägnant Ich habe auch die folgenden Erweiterungsmethoden zu 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); 
     } 
    } 

Dann kann ich tun:

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

myEvent.Publish("Testing"); 

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

Noch einmal, wenn Sie Ihre Gefühle meinen Sie Mocks verwenden möchten, gehen für sie. Wenn Sie lieber das echte Ding verwenden, funktioniert das.

Verwandte Themen