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

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

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;
}

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

13 ответов


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

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

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


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


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

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


при вызове диспетчера.BeginInvoke, вы инструктируете диспетчера запускать делегатов в своем потоке когда поток находится в режиме ожидания.

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

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


создание 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);
}

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


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

затем любой тестируемый класс, который имеет доступ к приложению.Текущий.Диспетчер найдет диспетчера.

поскольку в 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
    }
}

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

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

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

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

Я использую 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));
    }

Я предлагаю добавить еще один метод в 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;
        }
    }

Я выполнил это, обернув 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);
}

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

    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();
    }

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

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;
}