【问题标题】:Unit testing a class used in multi-threaded WPF application对多线程 WPF 应用程序中使用的类进行单元测试
【发布时间】:2016-11-26 11:50:12
【问题描述】:

我编写了一个 C# 类,它位于 WPF 应用程序的 UI 和异步消息传递系统之间。我现在正在为这个类编写单元测试,并遇到了调度程序的问题。以下方法属于我正在测试的类,它创建了一个订阅处理程序。所以我从单元测试 - 测试方法中调用这个Set_Listing_Records_ResponseHandler 方法。

public async Task<bool> Set_Listing_Records_ResponseHandler(
    string responseChannelSuffix, 
    Action<List<AIDataSetListItem>> successHandler, 
    Action<Exception> errorHandler)
{
    // Subscribe to Query Response Channel and Wire up Handler for Query Response
    await this.ConnectAsync();
    return await this.SubscribeTo_QueryResponseChannelAsync(responseChannelSuffix, new FayeMessageHandler(delegate (FayeClient client, FayeMessage message) {
        Application.Current.Dispatcher.BeginInvoke(new Action(() =>
        {
            try
            {
                ...
            }
            catch (Exception e)
            {
                ...
            }
        }));
    }));
}

执行流程回到 Application.Current.Dispatcher.... 行但随后抛出错误:

Object reference not set to an instance of an object.

当我调试时,我可以看到 Application.Current 为空。

我进行了一些搜索,发现了一些在单元测试 - 测试方法中使用调度程序的示例,我已经尝试了其中的一些,它们可以防止错误,但是调度程序中的代码永远不会运行。

我找不到任何在测试方法调用的方法中使用了 Dispatcher 的示例。

我在 Windows 10 机器上使用 .NET 4.5.2。

任何帮助将不胜感激。

感谢您的宝贵时间。

【问题讨论】:

  • 看看这里提供的答案,如果需要更多解释,请告诉我stackoverflow.com/a/38994745/5233410
  • 如果你希望你的类是可单元测试的,你应该遵循与依赖注入(DI)相关的常规良好实践。在这种情况下,您不应该使用 WPF 特定的类 (Dispatcher),尤其是 通过 WPF 特定的 static 类应用程序。相反,将 Dispatcher 视为依赖项并将其注入到您的类中,而不是 Dispatcher 本身,而是一些提供您需要的方法的接口。您应该自己创建和实现这样的接口,并将 WPF Dispatcher 包装在其中。然后在单元测试中,您只需提供此 IDispatcher 的另一个非 WPF 实现。

标签: c# wpf multithreading unit-testing .net-4.5.2


【解决方案1】:

正确的现代代码永远不会使用Dispatcher。它将您与特定的 UI 联系起来,并且难以进行单元测试(正如您所发现的那样)。

最佳方法是使用await 隐式捕获上下文并在其上恢复。例如,如果您知道 Set_Listing_Records_ResponseHandler 将始终从 UI 线程调用,那么您可以执行以下操作:

public async Task<bool> Set_Listing_Records_ResponseHandler(
    string responseChannelSuffix, 
    Action<List<AIDataSetListItem>> successHandler, 
    Action<Exception> errorHandler)
{
  // Subscribe to Query Response Channel and Wire up Handler for Query Response
  await this.ConnectAsync();
  var tcs = new TaskCompletionSource<FayeMessage>();
  await this.SubscribeTo_QueryResponseChannelAsync(responseChannelSuffix, new FayeMessageHandler(
      (client, message) => tcs.TrySetResult(message)));
  var message = await tcs.Task;
  // We are already back on the UI thread here; no need for Dispatcher.
  try
  {
    ...
  }
  catch (Exception e)
  {
    ...
  }
}

但是,如果您正在处理通知可能意外到达的事件类型的系统,那么您不能总是使用await 的隐式捕获。在这种情况下,次佳方法是在某个点(例如,对象构造)捕获当前的SynchronizationContext,并将您的工作排队到该位置,而不是直接发送到调度程序。例如,

private readonly SynchronizationContext _context;
public Constructor() // Called from UI thread
{
  _context = SynchronizationContext.Current;
}
public async Task<bool> Set_Listing_Records_ResponseHandler(
    string responseChannelSuffix, 
    Action<List<AIDataSetListItem>> successHandler, 
    Action<Exception> errorHandler)
{
  // Subscribe to Query Response Channel and Wire up Handler for Query Response
  await this.ConnectAsync();
  return await this.SubscribeTo_QueryResponseChannelAsync(responseChannelSuffix, new FayeMessageHandler(delegate (FayeClient client, FayeMessage message) {
    _context.Post(new SendOrPostCallback(() =>
    {
        try
        {
            ...
        }
        catch (Exception e)
        {
            ...
        }
    }, null));
  }));
}

但是,如果您觉得必须使用调度程序(或者只是不想现在清理代码),您可以使用WpfContext,它为单元测试提供了Dispatcher 实现。请注意,您仍然不能使用 Application.Current.Dispatcher(除非您使用 MSFakes)——您必须在某个时候捕获调度程序。

【讨论】:

    【解决方案2】:

    感谢所有回复的人,非常感谢您的反馈。

    这是我最终做的:

    我在 UICommsManager 类中创建了一个私有变量:

    private SynchronizationContext _MessageHandlerContext = null;
    

    并在 UICommsManager 的构造函数中初始化它。 然后我更新了我的消息处理程序以使用新的 _MessageHandlerContext 而不是 Dispatcher:

    public async Task<bool> Set_Listing_Records_ResponseHandler(string responseChannelSuffix, Action<List<AIDataSetListItem>> successHandler, Action<Exception> errorHandler)
    {
        // Subscribe to Query Response Channel and Wire up Handler for Query Response
        await this.ConnectAsync();
        return await this.SubscribeTo_QueryResponseChannelAsync(responseChannelSuffix, new FayeMessageHandler(delegate (FayeClient client, FayeMessage message) {
            _MessageHandlerContext.Post(delegate {
                try
                {
                    ...
                }
                catch (Exception e)
                {
                    ...
                }
            }, null);
        }));
    }
    

    在 UI 中使用时,UICommsManager 类将 SynchronizationContext.Current 传递给构造函数。

    我更新了我的单元测试以添加以下私有变量:

    private SynchronizationContext _Context = null;
    

    以及以下初始化它的方法:

    #region Test Class Initialize
    
    [TestInitialize]
    public void TestInit()
    {
        SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
        _Context = SynchronizationContext.Current;
    }
    
    #endregion
    

    然后 UICommsManager 将 _Context 变量从测试方法传递给它的构造函数。

    现在它可以在两种情况下工作,当被 WPF 调用时,当被单元测试调用时。

    【讨论】:

      猜你喜欢
      • 2010-09-11
      • 2019-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-05-27
      • 1970-01-01
      • 1970-01-01
      • 2012-03-21
      相关资源
      最近更新 更多