对多线程 WPF 应用程序中使用的 class 进行单元测试

Unit testing a class used in multi-threaded WPF application

我编写了一个 C# class,它介于 WPF 应用程序 UI 和异步消息系统之间。我现在正在为此 class 和 运行 编写单元测试以解决调度程序的问题。以下方法属于我正在测试的 class,它创建了一个订阅处理程序。所以我从单元测试 - 测试方法中调用这个 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 工作。

如有任何帮助,我们将不胜感激。

感谢您的宝贵时间。

正确的现代代码永远不会使用 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 的隐式捕获。在这种情况下,next best 方法是在某个时间点(例如,对象构造)捕获当前 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,它为 unit 提供了 Dispatcher 实现测试。请注意,您仍然不能使用 Application.Current.Dispatcher(除非您使用 MSFakes)——您必须在某个时候捕获调度程序。

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

这是我最后做的事情:

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

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 class 得到 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 调用时,当被单元测试调用时。