在单元测试中使用WPF Dispatcher

时间:2009-07-09 23:01:59

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

我无法让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永远不会在委托中运行代码。

16 个答案:

答案 0 :(得分:84)

通过使用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) 中找到此课程。

答案 1 :(得分:21)

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

答案 2 :(得分:16)

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

答案 3 :(得分:2)

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

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

要使此方面单元可测试,您必须更改基础设计,以便它不使用主线程的调度程序。另一种方法是利用 System.ComponentModel.BackgroundWorker 来修改不同线程上的用户。 (这只是一个例子,根据具体情况可能不合适。)


修改(5个月后) 我在不知道DispatcherFrame时写了这个答案。我很高兴在这个问题上出错了 - DispatcherFrame已经证明是非常有用的。

答案 4 :(得分: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);
}

答案 5 :(得分: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

答案 6 :(得分:2)

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

然后,任何访问Application.Current.Dispatcher的测试类都会找到一个调度程序。

因为AppDomain中只允许一个Application,所以我使用了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
    }
}

答案 7 :(得分:1)

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

这可能适合您的需求,也可能不适合您,至少对于测试与DependencyObject / WPF相关的内容来说,它总是足够的。

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

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

答案 8 :(得分:0)

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

答案 9 :(得分:0)

我建议在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 :(得分:0)

我通过在自己的IDispatcher接口中包装Dispatcher,然后使用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);
}

答案 11 :(得分:0)

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

答案 12 :(得分:0)

我来晚了,但这是我的方法:

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

答案 13 :(得分:0)

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

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

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

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

答案 14 :(得分:0)

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

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

在单元测试中,当您要等待调度程序事件完成处理时,请致电

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

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

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

答案 15 :(得分:0)

这是一个有点过时的帖子,BeginInvoke在今天并不是首选。 我一直在寻找模拟解决方案,却没有为InvokeAsync找到任何东西:

await App.Current.Dispatcher.InvokeAsync(() => 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);