代码之家  ›  专栏  ›  技术社区  ›  Thorsten Lorenz

如何在uithread上测试prism事件聚合器订阅?

  •  14
  • Thorsten Lorenz  · 技术社区  · 16 年前

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

    如前所述,很难模拟事件聚合器 here ,我只是实例化一个真正的实例并将其传递给正在测试的系统。

    在我的测试中,我然后通过那个聚合器发布事件,然后检查被测系统对它的反应。由于该事件将在生产过程中由filesystemwatcher引发,因此我希望通过订阅uithread来使用自动分派,以便在引发事件后更新ui。

    问题是,在测试期间,除非我不订阅uithread,否则在测试系统中永远不会注意到该事件。

    我正在使用mspec进行测试,我从vs2008内部通过tdd.net运行它。 添加 [RequiresSta] 去我的测试班没用

    有没有解决方案,可以避免我在测试期间更改thread选项(例如,通过一个属性-多么难看的黑客行为)????

    3 回复  |  直到 12 年前
        1
  •  18
  •   Arclight    12 年前

    如果您模拟事件和事件聚合器,并使用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
  •  15
  •   Anderson Imes    16 年前

    我真的认为你应该对所有东西使用mock,而不是eventaggregator。嘲笑一点都不难…我认为链接的答案并不能证明eventaggregator的可测试性。

    这是你的测试。我不使用mspec,但这里是moq的测试。你没有提供任何代码,所以我基于链接到的代码。您的场景比链接的场景稍微难一点,因为另一个操作只是想知道如何验证正在调用subscribe,但是您实际上想调用在subscribe中传递的方法……更困难的事,但不是很难。

    //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
  •  5
  •   Aaron Janes    13 年前

    你可能不喜欢这个,因为它可能涉及到你认为是“丑陋的黑客”,但我的偏好是使用一个真正的事件聚合器,而不是嘲笑一切。虽然表面上是一个外部资源,但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>
    

    再说一次,如果你的感性意味着你想用嘲笑,那就去做吧。如果你愿意用真品,这是可行的。

    推荐文章