针对 WPF 和 Dispatcher 的 NMock 问题测试
这是为线程迷准备的一个。我有这个方法:
public void RefreshMelts()
{
MeltsAvailable.Clear();
ThreadPool.QueueUserWorkItem(delegate
{
Dispatcher.BeginInvoke((ThreadStart)delegate
{
eventAggregator.GetEvent<BusyEvent>().Publish(true);
eventAggregator.GetEvent<StatusMessageEvent>().Publish(
new StatusMessage("Loading melts...", MessageSeverity.Low));
});
try
{
IList<MeltDto> meltDtos = meltingAppService.GetActiveMelts();
Dispatcher.Invoke((ThreadStart)delegate
{
foreach (MeltDto availableMelt in meltDtos)
{
MeltsAvailable.Add(availableMelt);
}
OnPropertyChanged("MeltsAvailable");
eventAggregator.GetEvent<BusyEvent>().Publish(false);
eventAggregator.GetEvent<StatusMessageEvent>().Publish(
new StatusMessage("Melts loaded", MessageSeverity.Low));
});
}
catch (ApplicationException ex)
{
log.Error("An error occurred in MeltsViewModel when attempting to load melts", ex);
Dispatcher.Invoke((ThreadStart)delegate
{
MeltsAvailable.Clear();
eventAggregator.GetEvent<StatusMessageEvent>().Publish(
new StatusMessage("Melt data could not be loaded because an error occurred; " +
"see the application log for detail",
MessageSeverity.High));
eventAggregator.GetEvent<BusyEvent>().Publish(false);
});
}
});
}
这是在 WPF 用户控件中定义的。 MeltsAvailable 是 MeltDtos 的 ObservableCollection。这段代码在应用程序本身中运行时效果很好。
问题是我想使用 NMock 创建一个单元测试来验证此方法的结果 - 具体来说,一旦调用它,MeltsAvailable 属性就会有一些项目。测试方法如下:
[TestMethod]
public void GetAvailableMeltsTest()
{
MeltDto mockMelt1 = new MeltDto();
MeltDto mockMelt2 = new MeltDto();
mockMelt1.MeltIdentifier = "TST0001";
mockMelt2.MeltIdentifier = "TST0002";
IList<MeltDto> availableMelts = new List<MeltDto>();
availableMelts.Add(mockMelt1);
availableMelts.Add(mockMelt2);
Expect.Exactly(1).On(service).Method("GetActiveMelts").Will(Return.Value(availableMelts));
MeltsViewModel vm = new MeltsViewModel(aggregator, logger, service, configManagerFactory); // All of these are mock objects
vm.RefreshMelts();
Thread.Sleep(millisecondDelayForEventPublish * 100);
mockery.VerifyAllExpectationsHaveBeenMet();
Assert.AreEqual(vm.MeltsAvailable.Count, 2);
Assert.AreEqual(vm.MeltsAvailable[0].MeltIdentifier, "TST0001");
Assert.AreEqual(vm.MeltsAvailable[1].MeltIdentifier, "TST0002");
}
测试在第一个 Assert.AreEqual 上始终失败。此时 vm.MeltsAvailable 为空。
如果我去掉所有线程并将其保留为:
public void RefreshMelts()
{
MeltsAvailable.Clear();
IList<MeltDto> meltDtos = meltingAppService.GetActiveMelts();
foreach (MeltDto availableMelt in meltDtos)
{
MeltsAvailable.Add(availableMelt);
}
OnPropertyChanged("MeltsAvailable");
}
测试通过。
因此,显然,它不喜欢线程 - 但即使打开“调试”->“异常”->“CLR 异常”->“抛出”,并关闭“仅我的代码”,我在 RefreshMelts 中也没有得到任何异常。
最奇怪的部分是,我将 MeltDto 对象加载到 MeltsAvailable 集合中的 Dispatcher.Invoke 调用似乎从未被调用。我可以用断点覆盖整个部分,而且它们永远不会被击中。在我的测试中将 Thread.Sleep 时间提高到甚至高达十秒也没有任何改变。
为什么?为什么该部分没有执行,为什么我不能进入它或闯入它,为什么我没有得到异常,为什么它在执行中工作正常但在测试中却不能?
非常感谢, 史蒂夫
Here's one for the threading junkies out there. I've got this method:
public void RefreshMelts()
{
MeltsAvailable.Clear();
ThreadPool.QueueUserWorkItem(delegate
{
Dispatcher.BeginInvoke((ThreadStart)delegate
{
eventAggregator.GetEvent<BusyEvent>().Publish(true);
eventAggregator.GetEvent<StatusMessageEvent>().Publish(
new StatusMessage("Loading melts...", MessageSeverity.Low));
});
try
{
IList<MeltDto> meltDtos = meltingAppService.GetActiveMelts();
Dispatcher.Invoke((ThreadStart)delegate
{
foreach (MeltDto availableMelt in meltDtos)
{
MeltsAvailable.Add(availableMelt);
}
OnPropertyChanged("MeltsAvailable");
eventAggregator.GetEvent<BusyEvent>().Publish(false);
eventAggregator.GetEvent<StatusMessageEvent>().Publish(
new StatusMessage("Melts loaded", MessageSeverity.Low));
});
}
catch (ApplicationException ex)
{
log.Error("An error occurred in MeltsViewModel when attempting to load melts", ex);
Dispatcher.Invoke((ThreadStart)delegate
{
MeltsAvailable.Clear();
eventAggregator.GetEvent<StatusMessageEvent>().Publish(
new StatusMessage("Melt data could not be loaded because an error occurred; " +
"see the application log for detail",
MessageSeverity.High));
eventAggregator.GetEvent<BusyEvent>().Publish(false);
});
}
});
}
This is defined in a WPF user control. MeltsAvailable is an ObservableCollection of MeltDtos. This code works beautifully when running in the application itself.
The trouble is that I would like to create a unit test, using NMock, to verify the results of this method - specifically, that once it's called, the MeltsAvailable property has some items. Here's the test method:
[TestMethod]
public void GetAvailableMeltsTest()
{
MeltDto mockMelt1 = new MeltDto();
MeltDto mockMelt2 = new MeltDto();
mockMelt1.MeltIdentifier = "TST0001";
mockMelt2.MeltIdentifier = "TST0002";
IList<MeltDto> availableMelts = new List<MeltDto>();
availableMelts.Add(mockMelt1);
availableMelts.Add(mockMelt2);
Expect.Exactly(1).On(service).Method("GetActiveMelts").Will(Return.Value(availableMelts));
MeltsViewModel vm = new MeltsViewModel(aggregator, logger, service, configManagerFactory); // All of these are mock objects
vm.RefreshMelts();
Thread.Sleep(millisecondDelayForEventPublish * 100);
mockery.VerifyAllExpectationsHaveBeenMet();
Assert.AreEqual(vm.MeltsAvailable.Count, 2);
Assert.AreEqual(vm.MeltsAvailable[0].MeltIdentifier, "TST0001");
Assert.AreEqual(vm.MeltsAvailable[1].MeltIdentifier, "TST0002");
}
The test consistently fails on the first Assert.AreEqual. vm.MeltsAvailable is empty at that point.
If I strip out all the threading and leave it just as:
public void RefreshMelts()
{
MeltsAvailable.Clear();
IList<MeltDto> meltDtos = meltingAppService.GetActiveMelts();
foreach (MeltDto availableMelt in meltDtos)
{
MeltsAvailable.Add(availableMelt);
}
OnPropertyChanged("MeltsAvailable");
}
The test passes.
So, obviously, there's something it doesn't like about the threads - but even turning on Debug->Exceptions->CLR Exceptions->Thrown, and turning off Just My Code, I get no exceptions at all in RefreshMelts.
The strangest part is that the Dispatcher.Invoke call where I load the MeltDto objects into the MeltsAvailable collection never seems to be called. I can blanket the whole section with breakpoints, and they never get hit. Boosting the Thread.Sleep time in my test to even as high as ten seconds changes nothing.
Why? Why is that section not executing, why can't I step into it or break into it, why am I not getting exceptions, why does it work fine in execution but not in a test?
Thanks much,
Steve
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
调度程序是一个与执行线程相关的消息循环。当主线程空闲时,它处理队列中的项目。在单元测试中,这种情况永远不会发生。线程很忙,然后在测试完成时退出。
如果您使用 Visual Studio 运行测试,则可以打开代码覆盖率突出显示,您将看到 Dispatcher.Invoke() 内的代码从未被调用(它将显示为红色)。
DispatcherFrame 可用于触发 Dispatcher 来处理排队的消息。将以下帮助器类添加到您的单元测试项目中:
在测试结束时(在断言之前)调用 DispatcherHelper.DoEvents()。这将触发调度程序处理未完成的事件,例如将项目添加到视图模型的可观察集合中的事件。然后,您可以检查视图模型的属性以验证它们是否设置正确。
The Dispatcher is a message loop that is tied to the executing thread. It processes the items in its queue when the main thread is idle. In a unit test, that never happens. The thread is busy and then it exits when the test is completed.
If you're using Visual Studio to run your tests, you can turn on code coverage highlighting and you'll see that code inside Dispatcher.Invoke() is never called (it will be displayed in red).
A DispatcherFrame can be used to trigger the Dispatcher to process queued messages. Add the following helper class to your unit test project:
At the end of your test (prior to the assertions) call DispatcherHelper.DoEvents(). This will trigger the Dispatcher to process outstanding events, such as the ones that add items to the view model's observable collection. You can then inspect the view model's properties to verify that they were set correctly.