Использование диспетчера WPF в модульных тестах - PullRequest
45 голосов
/ 10 июля 2009

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

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

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

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

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

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

Ответы [ 13 ]

85 голосов
/ 03 октября 2009

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

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

C # код:

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) .

21 голосов
/ 15 октября 2009

Мы решили эту проблему, просто высмеивая диспетчер за интерфейсом и извлекая интерфейс из нашего контейнера 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, который выполняет делегаты в фоновом потоке, но в большинстве случаев это не обязательно

16 голосов
/ 17 сентября 2009

Вы можете выполнить модульное тестирование с помощью диспетчера, вам просто нужно использовать 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.");
}

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

2 голосов
/ 03 апреля 2014

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

Тогда любой тестируемый класс, который обращается к 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
    }
}
2 голосов
/ 15 апреля 2013

Если вы хотите применить логику в ответе jbe к любому диспетчеру (не просто 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 можно найти в этой отличной статье .

.
2 голосов
/ 15 октября 2009

Создание 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);
}
2 голосов
/ 20 августа 2009

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

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

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


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

1 голос
/ 20 августа 2009

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

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

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

  • Если вы используете NUnit> = 2.5.0, есть атрибут [RequiresSTA], который может предназначаться для методов или классов тестирования. Однако будьте осторожны, если вы используете встроенный тестовый прогон, как, например, RN 4.5 NUnit, кажется, основан на более старой версии NUnit и не может использовать этот атрибут.
  • В более старых версиях NUnit вы можете настроить NUnit на использование потока [STAThread] с файлом конфигурации, см., Например, это сообщение в блоге от Chris Headgate.
  • Наконец, в той же записи блога есть запасной метод (который я успешно использовал в прошлом) для создания собственной ветки [STAThread] для запуска теста.
0 голосов
/ 28 августа 2018

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

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;
}
0 голосов
/ 30 октября 2017

Как насчет запуска теста в выделенном потоке с поддержкой 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();
    }
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...