2009-07-09 4 views
45

У меня возникли проблемы с получением диспетчера для запуска делегата, который я передаю ему при модульном тестировании. Все отлично работает, когда я запустить программу, но, во время тестирования устройства следующего код не будет работать:Использование диспетчера 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; 
} 

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

+0

Какая ошибка вы получаете? –

+0

У меня нет ошибки. То, что передается BeginInvoke на Диспетчере, никогда не запускается. –

+1

Я буду честен и скажу, что мне не пришлось тестировать модель представления, в которой пока еще используется диспетчер. Возможно ли, что диспетчер не работает. Вызов Dispatcher.CurrentDispatcher.Run() в вашей тестовой справке? Мне любопытно, поэтому опубликуйте результаты, если вы их получите. –

ответ

82

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

Вы можете написать простой вспомогательный метод 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).

+4

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

+0

Блестящий. Спасибо, что поделился. – ozczecho

+3

Это работало блестяще для меня. Это мой принятый ответ –

2

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

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

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


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

0

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

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

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

  • Если вы используете NUnit> = 2.5.0, есть атрибут [RequiresSTA], которые могут предназначаться методы испытаний или классы , Однако, если вы используете интегрированный тестовый бегун, как, например, R # 4.5 NUnit runner, похоже, основан на более старой версии NUnit и не может использовать этот атрибут.
  • С более старыми версиями NUnit вы можете установить NUnit для использования потока [STAThread] с конфигурационным файлом, см., Например, this blog post от Chris Headgate.
  • Наконец, the same blog post имеет метод возврата (который я успешно использовал в прошлом) для создания собственного потока [STAThread] для запуска теста.
15

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

Обнаружено об этом here.

+0

Да, только что вернулся, чтобы обновить этот вопрос, как я это сделал в конце. Я читаю то же сообщение, я думаю! –

2

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

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

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

Вот конкретная реализация зарегистрирован в контейнере МОК для реального приложения

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

+0

как издеваться над методом DispatcherInvoke? – lukaszk

+0

@lukaszk, в зависимости от вашей издевательской структуры, вы должны настроить метод Invoke на ваш макет, чтобы фактически запустить переданный ему делегат (если это то, что вам нужно). Вам необязательно запускать этот делегат, у меня есть некоторые тесты, где я просто проверяю, что правильный делегат был передан макету. –

2

Если вы хотите применить логику в jbe's 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, ознакомьтесь с этим excellent blog writeup.

+1

Это странное имя метода .. –

0

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

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

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

Тогда любой испытанный класс, доступ к 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 
    } 
} 
0

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

Я совершил это, обернув диспетчер в моем собственном интерфейсе 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); 
    } 
} 

Initializing диспетчер в своем классе при испытании:

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

Дразнящего диспетчеру внутри модульных тестов (в данном случае мой обработчик событий - OnMyEventHandler и принимает один параметр bool ca lled 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

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

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