Использование диспетчера WPF в модульных тестах

У меня возникли проблемы с получением диспетчера для запуска делегата, который я передаю ему при модульном тестировании. Все работает нормально, когда я запускаю программу, но во время unit test следующий код не будет запускаться:

this.Dispatcher.BeginInvoke(new ThreadStart(delegate
{
    this.Users.Clear();

    foreach (User user in e.Results)
    {
        this.Users.Add(user);
    }
}), DispatcherPriority.Normal, null);

У меня есть этот код в моем базовом классе viewmodel, чтобы получить диспетчер:

if (Application.Current != null)
{
    this.Dispatcher = Application.Current.Dispatcher;
}
else
{
    this.Dispatcher = Dispatcher.CurrentDispatcher;
}

Есть ли что-то, что мне нужно сделать, чтобы инициализировать Dispatcher для модульных тестов? Диспетчер никогда не запускает код в делегате.

Ответ 1

При использовании Visual Studio Unit Test Framework вам не нужно инициализировать Диспетчер самостоятельно. Вы абсолютно правы, что диспетчер автоматически не обрабатывает свою очередь.

Вы можете написать простой вспомогательный метод "DispatcherUtil.DoEvents()", который сообщает диспетчеру обрабатывать свою очередь.

Код С#:

public static class DispatcherUtil
{
    [SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
    public static void DoEvents()
    {
        DispatcherFrame frame = new DispatcherFrame();
        Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
            new DispatcherOperationCallback(ExitFrame), frame);
        Dispatcher.PushFrame(frame);
    }

    private static object ExitFrame(object frame)
    {
        ((DispatcherFrame)frame).Continue = false;
        return null;
    }
}

Вы также найдете этот класс в WPF Application Framework (WAF).

Ответ 2

Мы решили эту проблему, просто издеваясь над диспетчером за интерфейсом и вытаскивая интерфейс из нашего контейнера IOC. Здесь интерфейс:

public interface IDispatcher
{
    void Dispatch( Delegate method, params object[] args );
}

Здесь конкретная реализация, зарегистрированная в контейнере IOC для реального приложения

[Export(typeof(IDispatcher))]
public class ApplicationDispatcher : IDispatcher
{
    public void Dispatch( Delegate method, params object[] args )
    { UnderlyingDispatcher.BeginInvoke(method, args); }

    // -----

    Dispatcher UnderlyingDispatcher
    {
        get
        {
            if( App.Current == null )
                throw new InvalidOperationException("You must call this method from within a running WPF application!");

            if( App.Current.Dispatcher == null )
                throw new InvalidOperationException("You must call this method from within a running WPF application with an active dispatcher!");

            return App.Current.Dispatcher;
        }
    }
}

И вот макет, который мы поставляем коду во время модульных тестов:

public class MockDispatcher : IDispatcher
{
    public void Dispatch(Delegate method, params object[] args)
    { method.DynamicInvoke(args); }
}

У нас также есть вариант MockDispatcher, который выполняет делегаты в фоновом потоке, но не обязательно большую часть времени

Ответ 3

Вы можете unit test использовать диспетчер, вам просто нужно использовать DispatcherFrame. Ниже приведен пример одного из моих модульных тестов, который использует DispatcherFrame для принудительного выполнения очереди диспетчера.

[TestMethod]
public void DomainCollection_AddDomainObjectFromWorkerThread()
{
 Dispatcher dispatcher = Dispatcher.CurrentDispatcher;
 DispatcherFrame frame = new DispatcherFrame();
 IDomainCollectionMetaData domainCollectionMetaData = this.GenerateIDomainCollectionMetaData();
 IDomainObject parentDomainObject = MockRepository.GenerateMock<IDomainObject>();
 DomainCollection sut = new DomainCollection(dispatcher, domainCollectionMetaData, parentDomainObject);

 IDomainObject domainObject = MockRepository.GenerateMock<IDomainObject>();

 sut.SetAsLoaded();
 bool raisedCollectionChanged = false;
 sut.ObservableCollection.CollectionChanged += delegate(object sender, NotifyCollectionChangedEventArgs e)
 {
  raisedCollectionChanged = true;
  Assert.IsTrue(e.Action == NotifyCollectionChangedAction.Add, "The action was not add.");
  Assert.IsTrue(e.NewStartingIndex == 0, "NewStartingIndex was not 0.");
  Assert.IsTrue(e.NewItems[0] == domainObject, "NewItems not include added domain object.");
  Assert.IsTrue(e.OldItems == null, "OldItems was not null.");
  Assert.IsTrue(e.OldStartingIndex == -1, "OldStartingIndex was not -1.");
  frame.Continue = false;
 };

 WorkerDelegate worker = new WorkerDelegate(delegate(DomainCollection domainCollection)
  {
   domainCollection.Add(domainObject);
  });
 IAsyncResult ar = worker.BeginInvoke(sut, null, null);
 worker.EndInvoke(ar);
 Dispatcher.PushFrame(frame);
 Assert.IsTrue(raisedCollectionChanged, "CollectionChanged event not raised.");
}

Я узнал об этом здесь.

Ответ 4

Когда вы вызываете Dispatcher.BeginInvoke, вы поручаете диспетчеру запускать делегаты в потоке , когда поток неактивен.

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

Чтобы сделать этот модуль аспекта тестируемым, вам придется изменить базовый проект, чтобы он не использовал диспетчер основного потока. Другой альтернативой является использование System.ComponentModel.BackgroundWorker для изменения пользователей в другом потоке. (Это просто пример, он может быть неприемлемым в зависимости от контекста).


Изменить (5 месяцев спустя) Я написал этот ответ, не подозревая о DispatcherFrame. Я очень рад, что ошибся в этом: DispatcherFrame оказался чрезвычайно полезным.

Ответ 5

Создание DipatcherFrame отлично поработало для меня:

[TestMethod]
public void Search_for_item_returns_one_result()
{
    var searchService = CreateSearchServiceWithExpectedResults("test", 1);
    var eventAggregator = new SimpleEventAggregator();
    var searchViewModel = new SearchViewModel(searchService, 10, eventAggregator) { SearchText = searchText };

    var signal = new AutoResetEvent(false);
    var frame = new DispatcherFrame();

    // set the event to signal the frame
    eventAggregator.Subscribe(new ProgressCompleteEvent(), () =>
       {
           signal.Set();
           frame.Continue = false;
       });

    searchViewModel.Search(); // dispatcher call happening here

    Dispatcher.PushFrame(frame);
    signal.WaitOne();

    Assert.AreEqual(1, searchViewModel.TotalFound);
}

Ответ 6

Если вы хотите применить логику в jbe answer к любому диспетчеру (а не только к Dispatcher.CurrentDispatcher, вы можете использовать следующий метод расширения.

public static class DispatcherExtentions
{
    public static void PumpUntilDry(this Dispatcher dispatcher)
    {
        DispatcherFrame frame = new DispatcherFrame();
        dispatcher.BeginInvoke(
            new Action(() => frame.Continue = false),
            DispatcherPriority.Background);
        Dispatcher.PushFrame(frame);
    }
}

Использование:

Dispatcher d = getADispatcher();
d.PumpUntilDry();

Для использования с текущим диспетчером:

Dispatcher.CurrentDispatcher.PumpUntilDry();

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

Для дополнительного фона на DispatcherFrame, проверьте отличную запись блога.

Ответ 7

Я решил эту проблему, создав новое приложение в моей настройке unit test.

Тогда любой класс под тестом, который получает доступ к Application.Current.Dispatcher, найдет диспетчера.

Поскольку только одно приложение разрешено в AppDomain, я использовал AssemblyInitialize и помещал его в свой собственный класс ApplicationInitializer.

[TestClass]
public class ApplicationInitializer
{
    [AssemblyInitialize]
    public static void AssemblyInitialize(TestContext context)
    {
        var waitForApplicationRun = new TaskCompletionSource<bool>()
        Task.Run(() =>
        {
            var application = new Application();
            application.Startup += (s, e) => { waitForApplicationRun.SetResult(true); };
            application.Run();
        });
        waitForApplicationRun.Task.Wait();        
    }
    [AssemblyCleanup]
    public static void AssemblyCleanup()
    {
        Application.Current.Dispatcher.Invoke(Application.Current.Shutdown);
    }
}
[TestClass]
public class MyTestClass
{
    [TestMethod]
    public void MyTestMethod()
    {
        // implementation can access Application.Current.Dispatcher
    }
}

Ответ 8

Если ваша цель состоит в том, чтобы избежать ошибок при доступе к DependencyObject s, я предлагаю, чтобы явным образом вместо того, чтобы играть с потоками и Dispatcher, вы просто убедитесь, что ваши тесты выполняются в (одном) STAThread потоке.

Это может или не подходит вашим потребностям, для меня, по крайней мере, этого всегда было достаточно для тестирования чего-то связанного с DependencyObject/WPF.

Если вы хотите попробовать это, я могу указать на несколько способов сделать это:

  • Если вы используете NUnit >= 2.5.0, существует атрибут [RequiresSTA], который может настраивать методы тестирования или классы. Однако обратите внимание, что если вы используете интегрированный тестовый бегун, как, например, R # 4.5 NUnit-бегун, похоже, основан на более старой версии NUnit и не может использовать этот атрибут.
  • С более старыми версиями NUnit вы можете установить NUnit для использования потока [STAThread] с конфигурационным файлом, см., например, этот пост в блоге Крис Хэдгейт.
  • Наконец, тот же пост в блоге имеет метод резервного копирования (который я успешно использовал в прошлом) для создания собственного [STAThread] для запуска теста.

Ответ 9

Я использую технологию MSTest и Windows Forms с парадигмой MVVM. После того, как вы попробуете много решений, это (найдено в блоге Vincent Grondin) работает для меня:

    internal Thread CreateDispatcher()
    {
        var dispatcherReadyEvent = new ManualResetEvent(false);

        var dispatcherThread = new Thread(() =>
        {
            // This is here just to force the dispatcher 
            // infrastructure to be setup on this thread
            Dispatcher.CurrentDispatcher.BeginInvoke(new Action(() => { }));

            // Run the dispatcher so it starts processing the message 
            // loop dispatcher
            dispatcherReadyEvent.Set();
            Dispatcher.Run();
        });

        dispatcherThread.SetApartmentState(ApartmentState.STA);
        dispatcherThread.IsBackground = true;
        dispatcherThread.Start();

        dispatcherReadyEvent.WaitOne();
        SynchronizationContext
           .SetSynchronizationContext(new DispatcherSynchronizationContext());
        return dispatcherThread;
    }

И используйте его как:

    [TestMethod]
    public void Foo()
    {
        Dispatcher
           .FromThread(CreateDispatcher())
                   .Invoke(DispatcherPriority.Background, new DispatcherDelegate(() =>
        {
            _barViewModel.Command.Executed += (sender, args) => _done.Set();
            _barViewModel.Command.DoExecute();
        }));

        Assert.IsTrue(_done.WaitOne(WAIT_TIME));
    }

Ответ 10

Я предлагаю добавить еще один метод в DispatcherUtil, вызывая его DoEventsSync(), и просто вызовите диспетчера для вызова вместо BeginInvoke. Это необходимо, если вам действительно нужно подождать, пока Диспетчер обработает все кадры. Я отправляю это как другой ответ, а не просто комментарий, поскольку весь класс длинный:

    public static class DispatcherUtil
    {
        [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
        public static void DoEvents()
        {
            var frame = new DispatcherFrame();
            Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
                new DispatcherOperationCallback(ExitFrame), frame);
            Dispatcher.PushFrame(frame);
        }

        public static void DoEventsSync()
        {
            var frame = new DispatcherFrame();
            Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Background,
                new DispatcherOperationCallback(ExitFrame), frame);
            Dispatcher.PushFrame(frame);
        }

        private static object ExitFrame(object frame)
        {
            ((DispatcherFrame)frame).Continue = false;
            return null;
        }
    }

Ответ 11

Я выполнил это, обернув Dispatcher в моем собственном интерфейсе IDispatcher, а затем с помощью Moq, чтобы проверить, что вызов был сделан.

Интерфейс IDispatcher:

public interface IDispatcher
{
    void BeginInvoke(Delegate action, params object[] args);
}

Реальная реализация диспетчера:

class RealDispatcher : IDispatcher
{
    private readonly Dispatcher _dispatcher;

    public RealDispatcher(Dispatcher dispatcher)
    {
        _dispatcher = dispatcher;
    }

    public void BeginInvoke(Delegate method, params object[] args)
    {
        _dispatcher.BeginInvoke(method, args);
    }
}

Инициализация диспетчера в тестируемом классе:

public ClassUnderTest(IDispatcher dispatcher = null)
{
    _dispatcher = dispatcher ?? new UiDispatcher(Application.Current?.Dispatcher);
}

Извержение диспетчера внутри модульных тестов (в этом случае мой обработчик событий - OnMyEventHandler и принимает один параметр bool, называемый myBoolParameter)

[Test]
public void When_DoSomething_Then_InvokeMyEventHandler()
{
    var dispatcher = new Mock<IDispatcher>();

    ClassUnderTest classUnderTest = new ClassUnderTest(dispatcher.Object);

    Action<bool> OnMyEventHanlder = delegate (bool myBoolParameter) { };
    classUnderTest.OnMyEvent += OnMyEventHanlder;

    classUnderTest.DoSomething();

    //verify that OnMyEventHandler is invoked with 'false' argument passed in
    dispatcher.Verify(p => p.BeginInvoke(OnMyEventHanlder, false), Times.Once);
}

Ответ 12

Как запустить тест в выделенном потоке с поддержкой Dispatcher?

    void RunTestWithDispatcher(Action testAction)
    {
        var thread = new Thread(() =>
        {
            var operation = Dispatcher.CurrentDispatcher.BeginInvoke(testAction);

            operation.Completed += (s, e) =>
            {
                // Dispatcher finishes queued tasks before shuts down at idle priority (important for TransientEventTest)
                Dispatcher.CurrentDispatcher.BeginInvokeShutdown(DispatcherPriority.ApplicationIdle);
            };

            Dispatcher.Run();
        });

        thread.IsBackground = true;
        thread.TrySetApartmentState(ApartmentState.STA);
        thread.Start();
        thread.Join();
    }

Ответ 13

Я опоздал, но вот как я это делаю:

public static void RunMessageLoop(Func<Task> action)
{
  var originalContext = SynchronizationContext.Current;
  Exception exception = null;
  try
  {
    SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext());

    action.Invoke().ContinueWith(t =>
    {
      exception = t.Exception;
    }, TaskContinuationOptions.OnlyOnFaulted).ContinueWith(t => Dispatcher.ExitAllFrames(),
      TaskScheduler.FromCurrentSynchronizationContext());

    Dispatcher.Run();
  }
  finally
  {
    SynchronizationContext.SetSynchronizationContext(originalContext);
  }
  if (exception != null) throw exception;
}

Ответ 14

Самый простой способ, который я нашел, это просто добавить свойство, подобное этому, в любой ViewModel, которому нужно использовать Dispatcher:

public static Dispatcher Dispatcher => Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;

Таким образом, он работает как в приложении, так и при запуске модульных тестов.

Мне нужно было использовать его только в нескольких местах во всем приложении, поэтому я не возражал повторить себя.