17

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

由于这里提到的模拟事件聚合器有点困难,我只是实例化一个真实的聚合器并将其传递给被测系统。

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

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

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

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

4

3 回答 3

22

如果你同时模拟事件和事件聚合器,并使用 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
于 2012-03-13T23:30:17.880 回答
19

我真的认为你应该对所有事情都使用模拟,而不是 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);
于 2010-03-04T23:29:53.950 回答
5

你可能不喜欢这个,因为它可能涉及你认为“丑陋的黑客”,但我更喜欢使用真正的 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>

同样,如果你的感受意味着你想使用模拟,那就去吧。如果您宁愿使用真实的东西,这可行。

于 2013-10-10T21:47:57.537 回答