Как проверить подписки агрегатора событий Prism на UIThread? - PullRequest
14 голосов
/ 04 марта 2010

У меня есть класс, который подписывается на событие через агрегатор событий PRISMs.

Поскольку немного сложнее смоделировать агрегатор событий, как отмечено здесь , я просто создаю реальный экземпляр и передаю его тестируемой системе.

В моем тесте я публикую событие через этот агрегатор и проверяю, как тестируемая система реагирует на него. Так как событие будет вызвано FileSystemWatcher во время производства, я хочу использовать автоматическую рассылку, подписавшись на UIThread, поэтому я могу обновить свой пользовательский интерфейс, как только событие возникнет.

Проблема в том, что во время теста событие никогда не будет замечено в тестируемой системе, если я не подпишусь на UIThread.

Я использую MSpec для своих тестов, которые я запускаю из VS2008 через TDD.Net. Добавление [RequiresSta] в мой тестовый класс не помогло

У кого-нибудь есть решение, которое избавляет меня от изменения ThreadOption во время моих тестов (например, через свойство - что за уродливый взлом) ???

Ответы [ 3 ]

19 голосов
/ 14 марта 2012

Если вы издеваетесь над событием и агрегатором событий, и используете обратный вызов 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
16 голосов
/ 05 марта 2010

Я действительно думаю, что вы должны использовать mocks для всего, а не EventAggregator. Совсем не сложно издеваться ... Я не думаю, что связанный ответ доказывает многое о тестируемости EventAggregator.

Вот твой тест. Я не использую MSpec, но вот тест в Moq. Вы не предоставили никакого кода, поэтому я основываю его на связанном коде. Ваш сценарий немного сложнее, чем связанный сценарий, потому что другой OP просто хотел знать, как проверить, вызывается ли 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);
5 голосов
/ 11 октября 2013

Вам может не понравиться это, так как это может включать в себя то, что вы считаете "безобразным хаком", но я предпочитаю использовать настоящий EventAggregator, а не дразнить все.Хотя якобы внешний ресурс, EventAggregator работает в памяти и поэтому не требует большой настройки, очистки и не является узким местом, как другие внешние ресурсы, такие как базы данных, веб-сервисы и т. Д., И поэтому я чувствую этоподходит для использования в модульном тесте.Исходя из этого, я использовал этот метод для преодоления проблемы потока пользовательского интерфейса в NUnit с минимальными изменениями или риском для моего производственного кода ради тестов.

Сначала я создал метод расширения, например, так:

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>

Опять же, если ваши чувства означают, что вы хотите использовать макеты, сделайте это.Если вы предпочитаете использовать настоящую вещь, это работает.

...