Using the WPF Dispatcher in unit tests

前端 未结 16 2068
北恋
北恋 2020-11-27 02:48

I\'m having trouble getting the Dispatcher to run a delegate I\'m passing to it when unit testing. Everything works fine when I\'m running the program, but, during a unit te

相关标签:
16条回答
  • 2020-11-27 03:03

    When you call Dispatcher.BeginInvoke, you are instructing the dispatcher to run the delegates on its thread when the thread is idle.

    When running unit tests, the main thread will never be idle. It will run all of the tests then terminate.

    To make this aspect unit testable you will have to change the underlying design so that it isn't using the main thread's dispatcher. Another alternative is to utilise the System.ComponentModel.BackgroundWorker to modify the users on a different thread. (This is just an example, it might be innappropriate depending upon the context).


    Edit (5 months later) I wrote this answer while unaware of the DispatcherFrame. I'm quite happy to have been wrong on this one - DispatcherFrame has turned out to be extremely useful.

    0 讨论(0)
  • 2020-11-27 03:03

    I'm late but this is how I do it:

    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;
    }
    
    0 讨论(0)
  • 2020-11-27 03:06

    I accomplished this by wrapping Dispatcher in my own IDispatcher interface, and then using Moq to verify the call to it was made.

    IDispatcher interface:

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

    Real dispatcher implementation:

    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 dispatcher in your class under test:

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

    Mocking the dispatcher inside unit tests (in this case my event handler is OnMyEventHandler and accepts a single bool parameter called 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 讨论(0)
  • 2020-11-27 03:07

    By using the Visual Studio Unit Test Framework you don’t need to initialize the Dispatcher yourself. You are absolutely right, that the Dispatcher doesn’t automatically process its queue.

    You can write a simple helper method “DispatcherUtil.DoEvents()” which tells the Dispatcher to process its queue.

    C# Code:

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

    You find this class too in the WPF Application Framework (WAF).

    0 讨论(0)
  • 2020-11-27 03:10

    How about running the test on a dedicated thread with Dispatcher support?

        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();
        }
    
    0 讨论(0)
  • 2020-11-27 03:14

    If you want to apply the logic in jbe's answer to any dispatcher (not just Dispatcher.CurrentDispatcher, you can use the following extention method.

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

    Usage:

    Dispatcher d = getADispatcher();
    d.PumpUntilDry();
    

    To use with the current dispatcher:

    Dispatcher.CurrentDispatcher.PumpUntilDry();
    

    I prefer this variation because it can be used in more situations, is implemented using less code, and has a more intuitive syntax.

    For additional background on DispatcherFrame, check out this excellent blog writeup.

    0 讨论(0)
提交回复
热议问题