单元测试多线程WPF应用程序中使用的类

时间:2016-11-26 11:50:12

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

我编写了一个位于WPF应用程序的UI和Async Messaging System之间的C#类。我现在正在为这个类编写单元测试,并遇到调度程序的问题。以下方法属于我正在测试的类,它创建了一个订阅处理程序。所以我从单元测试 - 测试方法中调用这个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。

非常感谢任何协助。

感谢您的时间。

2 个答案:

答案 0 :(得分:0)

正确的现代代码永远不会使用Dispatcher。它将您绑定到特定的UI并使单元测试变得困难(如您所发现的那样)。

最佳方法是使用await隐式捕获上下文并在其上继续。例如,如果您知道将始终从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();
  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) - 您必须在某个时刻捕获调度程序。

答案 1 :(得分:0)

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

这是我最终做的事情:

我在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调用时,以及由单元测试调用时。