我编写了一个位于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。
非常感谢任何协助。
感谢您的时间。
答案 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调用时,以及由单元测试调用时。