【问题标题】:Using the WPF Dispatcher in unit tests在单元测试中使用 WPF Dispatcher
【发布时间】:2010-11-09 13:31:41
【问题描述】:

我无法让 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 以进行单元测试吗? Dispatcher 从不运行委托中的代码。

【问题讨论】:

  • 我没有收到任何错误。只是在 Dispatcher 上传递给 BeginInvoke 的内容永远不会运行。
  • 老实说,我还没有对使用调度程序的视图模型进行单元测试。调度程序是否可能没有运行。会在您的测试帮助中调用 Dispatcher.CurrentDispatcher.Run() 吗?我很好奇,所以如果你得到结果,请发布结果。

标签: c# .net wpf unit-testing dispatcher


【解决方案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
    }
}

【讨论】:

  • 我非常喜欢这个!
【解决方案2】:

通过使用 Visual Studio 单元测试框架,您无需自己初始化 Dispatcher。你说得对,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) 中找到此类。

【讨论】:

  • 我更喜欢这个答案而不是接受的答案,因为这个解决方案可以在按顺序编写的测试用例中运行,而接受的答案要求测试代码以面向回调的方法编写。跨度>
  • 不幸的是,这对我不起作用。对于感兴趣的人,这里也记录了此方法:MSDN DispatcherFrame 'DoEvents' Example
  • 忽略我最后的 cmets - 它工作正常,是测试 WPF 视图模型时解决此常见问题的好方法。
  • 每次被测系统必须处理 Dispatcher 时都应该调用这个方法,我对吗?还是每个单元测试会话只能调用一次此方法?
  • 您的 DoEvents 将冒险执行由 其他单元测试 安排的调用,从而导致非常难以调试的故障 :-( 我发现这篇文章是因为有人逐字添加将 DispatcherUtil 示例代码复制到我们的单元测试并导致了这个问题。我相信按照@OrionEdwards 的建议将调度程序隐藏在接口后面是一种更好的方法,尽管我会使用具有实际队列的实现和显式方法在单元测试中出队。如果我有时间实现它,我会在这里添加或编辑答案。
【解决方案3】:

这是一个有点旧的帖子,BeginInvoke 今天不是一个更好的选择。 我一直在寻找模拟的解决方案,但没有找到 InvokeAsync 的任何内容:

await App.Current.Dispatcher.InvokeAsync(() =&gt; something );

我添加了名为 Dispatcher 的新类,实现了 IDispatcher,然后注入到 viewModel 构造函数中:

public class Dispatcher : IDispatcher
{
    public async Task DispatchAsync(Action action)
    {
        await App.Current.Dispatcher.InvokeAsync(action);
    }
}
public interface IDispatcher
    {
        Task DispatchAsync(Action action);
    }

然后在测试中,我在构造函数中将 MockDispatcher 注入到 viewModel 中:

internal class MockDispatcher : IDispatcher
    {
        public async Task DispatchAsync(Action action)
        {
            await Task.Run(action);
        }
    }

在视图模型中使用:

await m_dispatcher.DispatchAsync(() => something);

【讨论】:

    【解决方案4】:

    Winforms 有一个非常简单且与 WPF 兼容的解决方案。

    从您的单元测试项目中,引用 System.Windows.Forms。

    当您想要等待调度程序事件完成处理时,从您的单元测试中调用

            System.Windows.Forms.Application.DoEvents();
    

    如果您有一个不断将 Invokes 添加到调度程序队列的后台线程,那么您将需要进行某种测试并继续调用 DoEvents 直到满足后台一些其他可测试的条件

            while (vm.IsBusy)
            {
                System.Windows.Forms.Application.DoEvents();
            }
    

    【讨论】:

      【解决方案5】:

      我发现的最简单的方法是向需要使用 Dispatcher 的任何 ViewModel 添加这样的属性:

      public static Dispatcher Dispatcher => Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
      

      这样它既可以在应用程序中工作,也可以在运行单元测试时工作。

      我只需要在整个应用程序中的几个地方使用它,所以我不介意重复一下。

      【讨论】:

      • 但是即使您使用 Dispatcher.CurrentDispatcher,您也必须让 Dispatcher 运行,并且在完成之前不要继续您的测试。
      • @DeniseSkidmore 运行测试时,CurrentDispatcher 只是成为运行测试的线程,否则您的测试方法将失败并出现空引用异常,因为 Application.Current 为空。这应该与测试的工作方式无关。
      • 对于单线程测试,这是真的。如果您的单元测试级别不够低,并产生另一个线程,然后使用 Dispatcher.CurrentDispatcher.Invoke,您需要允许它在您的主测试线程中运行。
      【解决方案6】:

      我迟到了,但我是这样做的:

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

      【讨论】:

        【解决方案7】:

        如何在具有 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();
            }
        

        【讨论】:

          【解决方案8】:

          我通过将 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 并接受一个名为 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);
          }
          

          【讨论】:

            【解决方案9】:

            我建议向 DispatcherUtil 添加一个方法,称为 DoEventsSync(),只需调用 Dispatcher 来调用而不是 BeginInvoke。如果您确实必须等到 Dispatcher 处理完所有帧,则需要这样做。我将其发布为另一个答案,而不仅仅是评论,因为整个课程都很长:

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

            【讨论】:

              【解决方案10】:

              我正在使用带有 MVVM 范例的 MSTestWindows Forms 技术。 在尝试了许多解决方案之后,这个(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));
                  }
              

              【讨论】:

                【解决方案11】:

                如果您想将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

                【讨论】:

                • Dispatcher.PushFrame(frame); 在内部使用Dispatcher.CurrentDispatcher...所以这行不通。
                【解决方案12】:

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

                【讨论】:

                  【解决方案13】:

                  您可以使用调度程序进行单元测试,您只需要使用 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

                  【讨论】:

                  • 是的,刚刚回来更新这个问题,我最终是如何做到的。我读了同样的帖子!
                  【解决方案14】:

                  当您调用 Dispatcher.BeginInvoke 时,您是在指示调度程序在其线程上运行委托当线程空闲时

                  在运行单元测试时,主线程将永远空闲。它将运行所有测试然后终止。

                  要使这方面的单元可测试,您必须更改底层设计,使其不使用主线程的调度程序。另一种选择是利用 System.ComponentModel.BackgroundWorker 来修改不同线程上的用户。 (这只是一个例子,根据上下文可能不合适)。


                  编辑(5 个月后) 我在不知道 DispatcherFrame 的情况下写了这个答案。我很高兴在这个问题上犯了错误 - DispatcherFrame 被证明非常有用。

                  【讨论】:

                    【解决方案15】:

                    我们通过简单地模拟出接口后面的调度程序并从我们的 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 的变体,它在后台线程中执行委托,但大多数时候都不需要

                    【讨论】:

                    • 如何模拟 DispatcherInvoke 方法?
                    • @lukaszk,根据您的模拟框架,您将在模拟上设置 Invoke 方法以实际运行传递给它的委托(如果这是您需要的行为)。您不一定需要运行该委托,我有一些测试,我只是验证正确的委托已传递给模拟。
                    • 对于那些使用起订量的人,这对我有用:` var mockDispatcher = new Mock(); mockDispatcher.Setup(dispatcher => dispatcher.Invoke(It.IsAny())).Callback(action => action());`
                    【解决方案16】:

                    如果您的目标是在访问DependencyObjects 时避免错误,我建议您不要明确使用线程和Dispatcher,而只需确保您的测试在(单个)STAThread 线程中运行。

                    这可能适合也可能不适合您的需求,至少对我而言,它一直足以测试任何与 DependencyObject/WPF 相关的内容。

                    如果你想试试这个,我可以告诉你几种方法:

                    • 如果您使用 NUnit >= 2.5.0,则有一个 [RequiresSTA] 属性可以针对测试方法或类。但请注意,如果您使用集成测试运行程序,例如 R#4.5 NUnit 运行程序似乎基于旧版本的 NUnit,并且无法使用此属性。
                    • 对于较旧的 NUnit 版本,您可以将 NUnit 设置为使用带有配置文件的 [STAThread] 线程,例如,请参阅 Chris Headgate 的 this blog post
                    • 最后,the same blog post 有一个备用方法(我过去曾成功使用过)来创建您自己的 [STAThread] 线程来运行您的测试。

                    【讨论】:

                    • 对于非常低级的单元测试,这是可行的,但有时您需要使用后台线程测试您的函数。
                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2010-09-24
                    • 2011-06-28
                    • 2023-03-17
                    相关资源
                    最近更新 更多