对多线程 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 调用时,当被单元测试调用时。
我编写了一个 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 调用时,当被单元测试调用时。