2010-03-03 17 views
13

J'ai une classe qui s'abonne à un événement via l'agrégateur d'événements PRISMs.Comment tester les abonnements à l'agrégateur d'événements Prism, sur l'UIThread?

Comme il est quelque peu difficile de se moquer de l'agrégateur d'événements comme noté here, je viens d'instancier un vrai et de le passer au système testé.

Dans mon test, je publie ensuite l'événement via cet agrégateur, puis je vérifie la réaction de mon système testé. Étant donné que l'événement sera déclenché par un FileSystemWatcher pendant la production, je souhaite utiliser l'envoi automatique en m'abonnant sur l'UIThread, afin de pouvoir mettre à jour mon interface utilisateur une fois l'événement levé. Le problème est que pendant le test, l'événement n'est jamais remarqué dans le système testé sauf si je ne m'abonne pas sur l'UIThread. J'utilise MSpec pour mes tests que je cours depuis VS2008 via TDD.Net. Ajout [RequiresSta] à ma classe de test n'a pas aidé

Quelqu'un at-il une solution, qui me sauve de changer le ThreadOption pendant mes tests (par exemple via une propriété - quelle bidouille horrible) ???

Répondre

16

Si vous simulez l'événement et l'aggrégateur d'événements et que vous utilisez le rappel de moq, vous pouvez le faire.

Voici un exemple:

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

Comment avez-vous répondu à cette question 2 ans après ma réponse avec la même solution et vous obtenez un crédit pour cela? :) –

+0

Avez-vous mis à jour votre réponse? Je ne me souviens pas avoir vu l'EA se moquer de votre réponse ... – TTat

+1

C'est la première ligne de ma réponse 'Mock eventAggregatorMock = nouveau Mock ();' –

15

Je pense vraiment que vous devriez utiliser des mocks pour tout et pas l'EventAggregator. Il n'est pas difficile de se moquer du tout ... Je ne pense pas que la réponse liée prouve une grande partie de la testabilité de l'EventAggregator.

Voici votre test. Je n'utilise pas MSpec, mais voici le test dans Moq. Vous n'avez fourni aucun code, donc je me base sur le code lié. Votre scénario est un peu plus difficile que le scénario lié car l'autre OP voulait juste savoir comment vérifier que l'abonnement était appelé, mais vous voulez réellement appeler la méthode qui a été passée dans l'abonnement ... quelque chose de plus difficile, mais pas très.

//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

Merci Anderson. Je mettrai à jour mon test avec votre suggestion quand j'y arriverai. Vous avez raison, se moquer de tout est la meilleure solution, je fais seulement des exceptions quand c'est quelque chose que vous supposez fonctionner, comme un composant de cadre (par exemple agrégateur d'événements) et pas trop lourd sur ressources et/ou lent. –

4

Vous ne pouvez pas aimer ce qu'il peut impliquer ce que vous ressentez est un « hack laid », mais ma préférence est d'utiliser un vrai EventAggregator plutôt que tout se moquant. Bien qu'apparemment une ressource externe, l'EventAggregator fonctionne en mémoire et ne nécessite donc pas beaucoup d'installation, d'effacement et n'est pas un goulot d'étranglement comme d'autres ressources externes telles que des bases de données, des services web, etc. est approprié à utiliser dans un test unitaire. Sur cette base, j'ai utilisé cette méthode pour surmonter le problème de thread UI dans NUnit avec un minimum de changement ou de risque à mon code de production pour le bien des tests.

Tout d'abord, je créé une méthode d'extension comme ceci:

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

}

Ensuite, dans tous mes abonnements d'événements j'utiliser les éléments suivants:

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

Dans le code de production, ce juste fonctionne parfaitement.Pour fins de test, tout ce que je dois faire est d'ajouter dans mon set-up avec un peu de code de synchronisation dans mon 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")); 
    } 
} 

Pour le Je attente et pulsant plus succinct a également ajouté les méthodes d'extension suivantes à 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); 
     } 
    } 

alors je peux faire:

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

myEvent.Publish("Testing"); 

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

Encore une fois, si votre sensibilité signifie que vous voulez utiliser simulacres, allez-y. Si vous préférez utiliser la vraie chose, cela fonctionne.