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

后端 未结 3 795
慢半拍i
慢半拍i 2020-12-31 04:07

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

相关标签:
3条回答
  • 2020-12-31 04:48

    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.

    0 讨论(0)
  • 2020-12-31 04:49

    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
    
    0 讨论(0)
  • 2020-12-31 05:01

    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);
    
    0 讨论(0)
提交回复
热议问题