在单元测试中使用WPF Dispatcher

54

我在单元测试时遇到了一个问题,无法使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未能执行委托中的代码。


我没有收到任何错误信息,只是传递给Dispatcher的BeginInvoke从未运行。 - Chris Shepherd
1
说实话,我还没有需要对使用调度程序的视图模型进行单元测试的经验。 调度程序是否没有运行?在您的测试中调用 Dispatcher.CurrentDispatcher.Run() 有帮助吗?我很好奇,如果您得到了结果,请发布它们。 - Anderson Imes
16个回答

1

我发现最简单的方法是在任何需要使用Dispatcher的ViewModel中添加一个属性,就像这样:

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

这样做可以使它在应用程序和运行单元测试时都能正常工作。

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


但是,即使您使用 Dispatcher.CurrentDispatcher,您仍然需要获取 Dispatcher 来运行,直到它完成才能继续进行测试。 - Denise Skidmore
@DeniseSkidmore 当运行测试时,CurrentDispatcher只是成为运行测试的线程,否则你要测试的方法将会因为Application.Current为空而失败。这与测试的工作方式无关。 - Shahin Dohan
对于单线程测试,这是正确的。如果您的单元测试不够低级,并且生成另一个线程,然后使用Dispatcher.CurrentDispatcher.Invoke,则需要允许其在主测试线程中运行。 - Denise Skidmore

1
如果你的目标是在访问DependencyObject时避免错误,我建议你不要明确处理线程和Dispatcher,而是确保你的测试在一个(单一的)STAThread线程中运行。
对于我来说,这可能适合你的需求,因为它总是足够测试任何与DependencyObject / WPF有关的东西。
如果你想尝试这个方法,我可以指向几种方法:
  • 如果您使用的是 NUnit >= 2.5.0,那么可以使用 [RequiresSTA] 属性来针对测试方法或类。但要注意,如果您使用集成测试运行器,例如 R#4.5 NUnit 运行器似乎基于较旧版本的 NUnit,无法使用此属性。
  • 对于较旧的 NUnit 版本,您可以通过配置文件设置 NUnit 使用 [STAThread] 线程,例如 Chris Headgate 的 这篇博客文章 中所述。
  • 最后,同一篇博客文章 还提供了一种备用方法(我过去曾成功使用过),用于创建自己的 [STAThread] 线程来运行测试。

对于非常低级别的单元测试,这是有效的,但有时您需要使用后台线程测试函数。 - Denise Skidmore

1

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

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

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

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

如果您有一个后台线程,不断向调度程序队列添加调用,则需要进行某种测试并不断调用DoEvents,直到满足后台其他可测试的条件。
        while (vm.IsBusy)
        {
            System.Windows.Forms.Application.DoEvents();
        }

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

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

0
我建议在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;
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接