How do I test Prism event aggregator subscriptions, on the UIThread?
Asked Answered
R

3

17

I have a class, that subscribes to an event via PRISMs event aggregator.

As it is somewhat hard to mock the event aggregator as noted here, I just instantiate a real one and pass it to the system under test.

In my test I then publish the event via that aggregator and then check how my system under test reacts to it. Since the event will be raised by a FileSystemWatcher during production, I want to make use of the automatic dispatch by subscribing on the UIThread, so I can update my UI once the event is raised.

The problem is, that during the test, the event never gets noticed in the system under test unless I don't subscribe on the UIThread.

I am using MSpec for my tests, which I run from inside VS2008 via TDD.Net. Adding [RequiresSta] to my test class didn't help

Does anyone have a solution, that saves me from changing the ThreadOption during my tests (e.g. via a property - what an ugly hack)???

Redan answered 3/3, 2010 at 21:37 Comment(0)
D
22

If you mock both the event and the Event Aggregator, and use moq's Callback, you can do it.

Here's an example:

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
Decontaminate answered 13/3, 2012 at 23:30 Comment(6)
How is it that you answered this 2 years after my answer with the same solution and you get credit for it? :)Hussite
Did you update your answer? I didn't remember seeing the EA mocked in your answer...Decontaminate
It's the first line of my answer Mock<IEventAggregator> eventAggregatorMock = new Mock<IEventAggregator>();Hussite
I dunno then...I can delete my answer if you want.Decontaminate
Nope, you are totally cool. I like how yours is organized and that you gave the answer for the long overload of Subscribe. Plus, I don't think you can delete your answer after it's been marked as an answer anyway. I was just complaining. Don't mind me.Hussite
other overload's of Subscribe are not virtual and hence can not user Setup on other overload's. This forces us to use the overload with all parameters (if we want to test later) even thought we might be happy with the default values passed to other argumentsBarrios
H
19

I really think you should use mocks for everything and not the EventAggregator. It's not hard to mock at all... I don't think the linked answer proves much of anything about the testability of the EventAggregator.

Here's your test. I don't use MSpec, but here's the test in Moq. You didn't provide any code, so I'm basing it on the linked-to code. Your scenario is a little harder than the linked scenario because the other OP just wanted to know how to verify that Subscribe was being called, but you actually want to call the method that was passed in the subscribe... something more difficult, but not very.

//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);
Hussite answered 4/3, 2010 at 23:29 Comment(1)
Thanks Anderson. I will update my test with your suggestion when I get around to it. You are right, mocking everything is the best solution, I only make exceptions when it is something you really assume to work, like a framework component (e.g. event aggregator)and it is not too heavy on resources and/or slow.Redan
J
5

You may not like this as it may involve what you feel is an "ugly hack", but my preference IS to use a real EventAggregator rather than mocking everything. While ostensibly an external resource, the EventAggregator runs in memory and so does not require much set-up, clear down, and is not a bottle neck like other external resources such as databases, web-services, etcetera would be and therefore I feel it is appropriate to use in a unit test. On that basis I have used this method to overcome the UI thread issue in NUnit with minimal change or risk to my production code for the sake of the tests.

Firstly I created an extension method like so:

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

}

Then, in all my event subscriptions I use the following:

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

In production code, this just works seamlessly. For testing purposes, all I have to do is add this in my set-up with a bit of synchronisation code in my 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"));
    }
}

To make the waiting and pulsing more succinct I have also added the following extension methods to 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);
        }
    }

Then I can do:

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

myEvent.Publish("Testing");

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

Again, if your sensibilities mean you want to use mocks, go for it. If you'd rather use the real thing, this works.

Judenberg answered 10/10, 2013 at 21:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.