代码之家  ›  专栏  ›  技术社区  ›  Chris Shepherd

在单元测试中使用WPF调度程序

  •  45
  • Chris Shepherd  · 技术社区  · 16 年前

    我无法让调度员运行一个我在单元测试时传递给它的委托。当我运行程序时,一切正常,但在单元测试期间,以下代码将不会运行:

    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 回复  |  直到 16 年前
        1
  •  83
  •   jbe    16 年前

    通过使用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) .

        2
  •  21
  •   Orion Edwards    16 年前

    我们解决了这个问题,只需模拟出接口后面的调度器,并从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 它在后台线程中执行委托,但大多数时候并不需要

        3
  •  16
  •   abatishchev Karl Johan    14 年前

    您可以使用分派器进行单元测试,只需要使用分派器框架。下面是我的一个单元测试示例,它使用DispatcherFrame强制执行Dispatcher队列。

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

        4
  •  2
  •   Andrew Shepherd    16 年前

    调用Dispatcher.BeginInvoke时,将指示Dispatcher在其线程上运行委托。 当线程空闲时 .

    运行单元测试时,主线程将 从未 无所事事。它将运行所有测试,然后终止。

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


    编辑 (5个月后) 我在不知道调度程序框架的情况下写了这个答案。我很高兴在这个单调度器框架上出错,结果发现它非常有用。

        5
  •  2
  •   abatishchev Karl Johan    14 年前

    创建双色框架对我来说非常有用:

    [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);
    }
    
        6
  •  2
  •   Community Mohan Dere    9 年前

    如果要在 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 .

        7
  •  2
  •   informatorius    10 年前

    我通过在单元测试设置中创建一个新的应用程序来解决这个问题。

    然后,任何访问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
        }
    }
    
        8
  •  1
  •   Thomas Dufour    16 年前

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

    这可能或不适合您的需要,至少对我来说,它已经足够测试任何与DependencyObject/WPF相关的内容了。

    如果你想试试这个,我可以给你指出几种方法:

    • 如果使用nunit>=2.5.0,则 [RequiresSTA] 可以以测试方法或类为目标的属性。但是,如果您使用集成的测试运行程序,例如R 4.5 nunit运行程序似乎基于较旧版本的nunit,并且不能使用此属性,则要小心。
    • 使用旧的nunit版本,可以将nunit设置为使用 [STAThread] 带有配置文件的线程,请参见例如 this blog post 由Chris Headgate提供。
    • 最后, the same blog post 有一个回退方法(我过去成功使用过这个方法)来创建您自己的 [标准螺纹] 运行测试的线程。
        9
  •  0
  •   Tomasito    12 年前

    我在用 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));
        }
    
        10
  •  0
  •   thewhiteambit    12 年前

    我建议在DispatcherUtil调用doEventsSync()中再添加一个方法,只需调用Dispatcher来调用而不是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;
            }
        }
    
        11
  •  0
  •   Eternal21    9 年前

    我通过将调度器包装在自己的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的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);
    }
    
        12
  •  0
  •   Esge    8 年前

    在支持调度程序的专用线程上运行测试如何?

        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();
        }
    
        13
  •  0
  •   Andreas Zita    7 年前

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

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