【问题标题】:How do I test Prism event aggregator subscriptions, on the UIThread?如何在 UIThread 上测试 Prism 事件聚合器订阅?
【发布时间】:2010-03-03 21:37:29
【问题描述】:

我有一个类,它通过 PRISM 事件聚合器订阅一个事件。

由于像 here 中提到的那样模拟事件聚合器有点困难,所以我只实例化一个真实的聚合器并将其传递给被测系统。

在我的测试中,然后我通过该聚合器发布事件,然后检查我的被测系统如何对其做出反应。由于该事件将在生产过程中由 FileSystemWatcher 引发,因此我想通过订阅 UIThread 来利用自动调度,以便在引发事件后更新我的 UI。

问题是,在测试过程中,除非我没有订阅 UIThread,否则在被测系统中不会注意到该事件。

我正在使用 MSpec 进行测试,我通过 TDD.Net 从 VS2008 内部运行。 将[RequiresSta] 添加到我的测试类并没有帮助

有没有人有解决方案,可以让我在测试期间免于更改 ThreadOption(例如,通过属性 - 多么丑陋的 hack)???

【问题讨论】:

    标签: c# tdd prism ui-thread eventaggregator


    【解决方案1】:

    如果你同时模拟事件和事件聚合器,并使用 moq 的回调,你可以做到。

    这是一个例子:

    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
    

    【讨论】:

    • 在我用相同的解决方案回答我的答案 2 年后,您如何回答这个问题并因此获得荣誉? :)
    • 你更新你的答案了吗?我不记得在您的回答中看到 EA 被嘲笑...
    • 这是我回答的第一行Mock&lt;IEventAggregator&gt; eventAggregatorMock = new Mock&lt;IEventAggregator&gt;();
    • 不,你很酷。我喜欢你的组织方式,并且你为Subscribe 的长期超载给出了答案。另外,无论如何,我认为您不能在将答案标记为答案后将其删除。我只是在抱怨。别介意我。
    • 订阅的其他重载不是虚拟的,因此不能用户设置其他重载。这迫使我们对所有参数使用重载(如果我们想稍后测试),即使我们可能对传递给其他参数的默认值感到满意
    【解决方案2】:

    我真的认为你应该对所有事情都使用模拟,而不是 EventAggregator。一点也不难嘲笑...我认为链接的答案并不能证明 EventAggregator 的可测试性。

    这是你的测试。我不使用 MSpec,但这是 Moq 中的测试。您没有提供任何代码,因此我将其基于链接到的代码。您的场景比链接的场景要难一些,因为其他 OP 只是想知道如何验证是否正在调用订阅,但您实际上想调用订阅中传递的方法......更困难,但不是非常。

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

    【讨论】:

    • 谢谢安德森。当我开始使用它时,我会用你的建议更新我的测试。你是对的,模拟一切是最好的解决方案,我只在你真正认为可以工作的东西时例外​​,比如框架组件(例如事件聚合器)并且它不是资源太重和/或速度太慢。
    【解决方案3】:

    您可能不喜欢这样,因为它可能涉及您认为“丑陋的 hack”,但我更喜欢使用真正的 EventAggregator,而不是嘲笑一切。虽然表面上是外部资源,但 EventAggregator 在内存中运行,因此不需要太多设置、清理,并且不像其他外部资源(如数据库、Web 服务等)那样成为瓶颈,因此我觉得适合在单元测试中使用。在此基础上,我使用这种方法来克服 NUnit 中的 UI 线程问题,并且为了测试对我的生产代码的更改或风险最小。

    首先我创建了一个这样的扩展方法:

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

    }

    然后,在我的所有活动订阅中,我使用以下内容:

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

    在生产代码中,这可以无缝运行。出于测试目的,我所要做的就是在我的设置中添加这个,并在我的测试中添加一些同步代码:

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

    为了让等待和脉冲更简洁,我还在 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);
            }
        }
    

    那我可以做:

    // <snip>
    aggregator.Subscribe(x => locker.Pulse(), ThreadOption.UIThread.MakeSafe());
    
    myEvent.Publish("Testing");
    
    locker.Wait(1000);
    // </snip>
    

    再说一次,如果您的感受意味着您想使用模拟,那就去吧。如果你宁愿使用真实的东西,这行得通。

    【讨论】:

      猜你喜欢
      • 2010-12-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-01-23
      • 2011-03-23
      相关资源
      最近更新 更多