C# 在单元测试中使用 WPF Dispatcher

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1106881/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-06 08:24:00  来源:igfitidea点击:

Using the WPF Dispatcher in unit tests

c#.netwpfunit-testingdispatcher

提问by Chris Shepherd

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 test the following code will not run:

我在让 Dispatcher 运行单元测试时传递给它的委托时遇到问题。当我运行程序时一切正常,但是,在单元测试期间,以下代码不会运行:

this.Dispatcher.BeginInvoke(new ThreadStart(delegate
{
    this.Users.Clear();

    foreach (User user in e.Results)
    {
        this.Users.Add(user);
    }
}), DispatcherPriority.Normal, null);

I have this code in my viewmodel base class to get a Dispatcher:

我在我的 viewmodel 基类中有这个代码来获取调度程序:

if (Application.Current != null)
{
    this.Dispatcher = Application.Current.Dispatcher;
}
else
{
    this.Dispatcher = Dispatcher.CurrentDispatcher;
}

Is there something I need to do to initialise the Dispatcher for unit tests? The Dispatcher never runs the code in the delegate.

我需要做些什么来初始化单元测试的调度程序?Dispatcher 从不运行委托中的代码。

采纳答案by jbe

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.

通过使用 Visual Studio 单元测试框架,您无需自己初始化 Dispatcher。您是绝对正确的,Dispatcher 不会自动处理其队列。

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

您可以编写一个简单的辅助方法“DispatcherUtil.DoEvents()”,它告诉 Dispatcher 处理其队列。

C# Code:

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

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

您也可以在WPF 应用程序框架 (WAF) 中找到此类。

回答by Andrew Shepherd

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

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

When running unit tests, the main thread will neverbe 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.BackgroundWorkerto modify the users on a different thread. (This is just an example, it might be innappropriate depending upon the context).

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



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.

编辑(5 个月后)我在不知道 DispatcherFrame 的情况下写了这个答案。我很高兴在这一点上错了 - DispatcherFrame 非常有用。

回答by Thomas Dufour

If your goal is to avoid errors when accessing DependencyObjects, I suggest that, rather than playing with threads and Dispatcherexplicitly, you simply make sure that your tests run in a (single) STAThreadthread.

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

This may or may not suit your needs, for me at least it has always been enough for testing anything DependencyObject/WPF-related.

这可能适合也可能不适合您的需求,至少对我来说它总是足以测试任何与 DependencyObject/WPF 相关的东西。

If you wish to try this, I can point you to several ways to do this :

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

  • If you use NUnit >= 2.5.0, there is a [RequiresSTA]attribute that can target test methods or classes. Beware though if you use an integrated test runner, as for example the R#4.5 NUnit runner seems to be based on an older version of NUnit and cannot use this attribute.
  • With older NUnit versions, you can set NUnit to use a [STAThread]thread with a config file, see for example this blog postby Chris Headgate.
  • Finally, the same blog posthas a fallback method (which I've successfully used in the past) for creating your own [STAThread]thread to run your test on.
  • 如果您使用 NUnit >= 2.5.0,则有一个[RequiresSTA]属性可以针对测试方法或类。但请注意,如果您使用集成的测试运行程序,例如 R#4.5 NUnit 运行程序似乎基于旧版本的 NUnit,并且不能使用此属性。
  • 对于较旧的 NUnit 版本,您可以将 NUnit 设置为使用[STAThread]带有配置文件的线程,例如参见Chris Headgate 的这篇博客文章
  • 最后,同一篇博文有一个回退方法(我过去曾成功使用过),用于创建您自己的[STAThread]线程来运行您的测试。

回答by StewartArmbrecht

You can unit test using a dispatcher, you just need to use the DispatcherFrame. Here is an example of one of my unit tests that uses the DispatcherFrame to force the dispatcher queue to execute.

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

I found out about it here.

我在这里发现了它。

回答by jbe

Creating a DipatcherFrame worked great for me:

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

回答by Orion Edwards

We've solved this issue by simply mocking out the dispatcher behind an interface, and pulling in the interface from our IOC container. Here's the interface:

我们通过简单地模拟接口后面的调度程序并从我们的 IOC 容器中拉入接口来解决了这个问题。这是界面:

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

Here's the concrete implementation registered in the IOC container for the real app

下面是真实应用在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;
        }
    }
}

And here's a mock one that we supply to the code during unit tests:

这是我们在单元测试期间提供给代码的模拟:

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

We also have a variant of the MockDispatcherwhich executes delegates in a background thread, but it's not neccessary most of the time

我们还有一个MockDispatcher在后台线程中执行委托的变体,但大多数时候这不是必需的

回答by Timothy Schoonover

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

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

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.

有关更多背景信息DispatcherFrame,请查看这篇优秀的博客文章

回答by Tomasito

I'm using MSTestand Windows Formstechnology with MVVM paradigm. After trying many solutions finally this (found on Vincent Grondin blog)works for me:

我使用MSTestWindows Forms技术与MVVM范例。在尝试了许多解决方案后,这(在 Vincent Grondin 博客上找到)最终对我有用

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

And use it like:

并像这样使用它:

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

回答by informatorius

I solved this problem by creating a new Application in my unit test setup.

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

Then any class under test which access to Application.Current.Dispatcher will find a dispatcher.

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

Because only one Application is allowed in an AppDomain I used the AssemblyInitialize and put it into its own class ApplicationInitializer.

因为 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
    }
}

回答by thewhiteambit

I suggest adding one more method to the DispatcherUtil call it DoEventsSync() and just call the Dispatcher to Invoke instead of BeginInvoke. This is needed if you really have to wait until the Dispatcher processed all frames. I am posting this as another Answer not just a comment, since the whole class is to long:

我建议在 DispatcherUtil 中再添加一种方法,将其称为 DoEventsSync(),并且只调用 Dispatcher 来 Invoke 而不是 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;
        }
    }