52

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

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

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

我的视图模型基类中有这段代码来获取调度程序:

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

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

4

16 回答 16

92

通过使用 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 应用程序框架 (WAF)中找到此类。

于 2009-10-03T10:12:27.603 回答
25

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

于 2009-10-14T22:21:18.403 回答
17

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

我在这里发现了它。

于 2009-09-17T12:05:57.493 回答
6

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

然后,任何访问 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
    }
}
于 2014-04-03T09:59:39.910 回答
2

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

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

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


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

于 2009-08-20T03:17:14.603 回答
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);
}
于 2009-10-14T22:08:06.417 回答
2

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

用法:

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

与当前调度程序一起使用:

Dispatcher.CurrentDispatcher.PumpUntilDry();

我更喜欢这种变体,因为它可以在更多情况下使用,使用更少的代码实现,并且语法更直观。

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

于 2013-04-15T19:20:30.570 回答
1

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

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

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

  • 如果您使用 NUnit >= 2.5.0,则有一个[RequiresSTA]属性可以针对测试方法或类。但请注意,如果您使用集成测试运行程序,例如 R#4.5 NUnit 运行程序似乎基于旧版本的 NUnit,并且无法使用此属性。
  • 对于较旧的 NUnit 版本,您可以将 NUnit 设置为使用[STAThread]带有配置文件的线程,例如,请参阅Chris Headgate 的这篇博客文章
  • 最后,同一篇博文有一个备用方法(我过去曾成功使用过)来创建您自己的[STAThread]线程来运行您的测试。
于 2009-08-20T09:31:01.717 回答
1

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

并像这样使用它:

    [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));
    }
于 2014-03-02T12:29:40.427 回答
1

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

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

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

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

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

        while (vm.IsBusy)
        {
            System.Windows.Forms.Application.DoEvents();
        }
于 2019-09-16T21:40:10.347 回答
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;
        }
    }
于 2014-06-23T10:53:54.627 回答
0

我通过将 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);
}
于 2016-08-23T12:26:53.013 回答
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();
    }
于 2017-10-30T02:09:19.547 回答
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;
}
于 2018-08-28T10:57:16.283 回答
0

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

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

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

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

于 2019-07-10T11:20:10.780 回答
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);
于 2020-04-09T13:35:37.673 回答