it-swarm.com.ru

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

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

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 для модульных тестов? Диспетчер никогда не запускает код в делегате.

45
Chris Shepherd

Используя 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).

83
jbe

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

21
Orion Edwards

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

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

16
StewartArmbrecht

Создание 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
Jon Dalberg

Если вы хотите применить логику в ответе 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
Timothy Schoonover

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

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

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


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

2
Andrew Shepherd

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

Тогда любой тестируемый класс, который имеет доступ к 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
informatorius

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

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

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

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

Как насчет запуска теста в выделенном потоке с поддержкой 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();
    }
0
Esge

Я сделал это, обернув 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);
}
0
Eternal21

Я предлагаю добавить еще один метод в DispatcherUtil, называть его DoEventsSync () и просто вызывать Dispatcher для Invoke вместо 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;
        }
    }
0
thewhiteambit

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

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
Andreas Zita

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

    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));
    }
0
Tomasito