在unit testing中使用WPF Dispatcher

我很难让分派器运行一个委托,我unit testing时传递给它。 运行程序时一切正常,但在unit testing期间,以下代码将不会运行:

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

有什么我需要做的初始化单位testing分派器? 分派器从不运行委托中的代码。

通过使用Visual Studiounit testing框架,您不需要自己初始化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应用程序框架(WAF)中find此类。

我们通过简单地模拟接口后面的调度器,并从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; } } } 

下面是我们在unit testing中提供给代码的一个模拟代码:

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

我们也有一个MockDispatcher的变体,它在后台线程中执行委托,但大部分时间不是必需的

你可以使用调度器进行unit testing,你只需要使用DispatcherFrame。 下面是一个使用DispatcherFrame强制执行分派器队列的unit testing示例。

 [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."); } 

我在这里发现了它。

调用Dispatcher.BeginInvoke时,您正在指示调度程序在线程空闲时在其线程上运行代理

运行unit testing时,主线程不会空闲。 它将运行所有的testing,然后终止。

为了使这个方面单元可testing,你将不得不改变底层devise,以便它不使用主线程的调度器。 另一种方法是利用System.ComponentModel.BackgroundWorker来修改不同线程上的用户。 (这只是一个例子,根据具体情况可能是不合适的)。


编辑 (5个月后)我写了这个答案,而不知道的DispatcherFrame。 我很高兴在这一个错误 – DispatcherFrame已经certificate是非常有用的。

创build一个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的答案应用于任何调度器(不只是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(); 

我更喜欢这个变体,因为它可以在更多的情况下使用,使用更less的代码来实现,并且具有更直观的语法。

有关DispatcherFrame其他背景,请查看这个优秀的博客写作 。

我通过在unit testing设置中创build一个新的应用程序来解决这个问题。

然后,任何访问Application.Current.Dispatcher的被testing类将find一个调度器。

因为在一个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 } } 

如果您的目标是在访问DependencyObject时避免错误,我build议您不要显式地使用线程和Dispatcher ,而只需确保testing运行在(单个) STAThread线程中。

这可能会也可能不适合您的需求,至less对于testing任何与DependencyObject / WPF相关的东西来说,这已经足够了。

如果你想尝试这个,我可以指出几种方法来做到这一点:

  • 如果使用NUnit> = 2.5.0,则有一个可以定位testing方法或类的[RequiresSTA]属性。 但是要小心,如果你使用集成的testing运行器,例如R#4.5 NUnit runner似乎是基于旧版本的NUnit,并且不能使用这个属性。
  • 对于较旧的NUnit版本,可以将NUnit设置为使用带有configuration文件的[STAThread]线程,例如,请参阅Chris Headgate撰写的此博客文章 。
  • 最后, 同样的博客文章有一个回退方法(我已经成功地使用过)创build自己的[STAThread]线程来运行testing。

我正在使用MSTestWindows Forms技术与MVVM范例。 终于尝试了许多解决scheme后(在Vincent Grondin博客上find)为我工作:

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

我build议增加一个方法到DispatcherUtil调用DoEventsSync(),只是调用Dispatcher调用,而不是BeginInvoke。 如果您真的必须等到分派器处理完所有帧,才需要这样做。 我发布这个作为另一个答案不仅仅是一个评论,因为整个class级是很长的:

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

我通过在我自己的IDispatcher接口中包装Dispatcher来完成这个任务,然后使用Moq来validation对它的调用。

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

初始化您的class级中的调度员:

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

嘲笑unit testing内部的调度器(在这种情况下,我的事件处理程序是OnMyEventHandler并接受一个名为myBoolParameter的单一的bool参数)

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

如何使用Dispatcher支持在专用线程上运行testing?

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