我正在运行一个简单的WPF应用程序,该应用程序使用基于事件的/ TPL方法来处理数据。 本示例中使用了三个类(视图,演示者,模型)
Snip of Presenter:
internal void btn_test_Click(object sender, EventArgs e)
{
Task<Person>.Factory.StartNew(() => GetPerson(id)).ContinueWith(UpdateTest, TaskScheduler.FromCurrentSynchronizationContext());
}
public Person GetPerson(int id)
{
Person p = Model.GetPerson(id);
return p;
}
private void UpdateTest(Task<Person> task)
{
Person p = task.Result;
window.tb_test.Text = p.ID + " " + p.Name; // PROBLEM HERE
}
所以,我从View中获取一个事件,启动一个新任务以从我的数据库或服务获取数据,然后更新UI。工作得非常好。
现在我想为这个场景创建一个单元测试。显示的值是否正确?
[TestMethod]
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
waitHandle = new ManualResetEvent(false);
WPF.MainWindowView mwv = new MainWindowView();
mwv.btn_test.RaiseEvent(new RoutedEventArgs(ButtonBase.ClickEvent));
mwv.tb_test.TextChanged += (s, e) => waitHandle.Set();
waitHandle.WaitOne();
Assert.AreEqual("43 displayvalue", mwv.tb_test.Text);
WPF应用程序工作正常,但测试时出现InvalidoperationException。我尝试使用Dispatcher通过调用
来更新UI组件window.tb_test.Dispatcher.BeginInvoke((ThreadStart)delegate {window.tb_test.Text = t.ID + " " + t.Name});
在UpdateTest中,但是在我的测试模块中没有调用“tb_test.textChanged”事件,尽管app本身运行良好。
答案 0 :(得分:0)
假设window
是WPF Window
而tb_test
是WPF TextBlock
。
首先,让我先说一下WPF线程模型使得对实时WPF对象运行单元测试变得有点麻烦。就个人而言,我发现这种编码测试的好处是最小的,与处理所有这些的麻烦相比,特别是在遵循MVVM设计模式时。将重要逻辑移动到更适合测试的位置(读取:数据绑定和操作视图模型对象的命令)会使这些测试看起来更加冗余。
如果您可以重新设计您的设计,以便您可以在一个更可测试的位置获得所有“重要”逻辑,那么我会建议尝试。我不知道这个代码库的历史是什么,所以我真的不想留下它。如果你不能,和/或如果你只是好奇,那就让我们走下兔子洞......
在测试中设置当前SynchronizationContext
时,您使用的是SynchronizationContext
Post
,ThreadPool
是使用TaskScheduler.FromCurrentSynchronizationContext
实现的(即,回调可以在任何线程上执行)。因此,当TextBlock
计划任务延续时,它会在可能是不同的线程上运行“更新Text
的{{1}}”代码,打破WPF的“必须执行”在Dispatcher
线程“规则。
如果Dispatcher.BeginInvoke
正在运行,您建议的使用Dispatcher
的修补程序可能会解决您的问题。我在发布的测试代码中的任何地方都没有看到Dispatcher.Run
或Dispacher.PushFrame
,因此我认为有效地将在Dispatcher
上执行的任何内容转换为无操作(进入永远不会被读取的队列)。当应用程序正常运行时,Visual Studio为您自动生成的代码会在可执行文件的入口点结束时调用Application.Run
,这会(最终)为您调用Dispatcher.Run
以便它可以开始处理诸如“显示主窗口”等消息。
你可能会注意到在调用Dispatcher.Run
之后,它会阻止调度程序在你调用它的任何线程上,直到你告诉它从另一个线程关闭。告诉它关闭后,没有办法在该线程上启动另一个Dispatcher
...所以从本质上讲,要么每个测试需要旋转并旋转它自己的单独线程(如果你想要的话,烦人的慢)写一下这些类型的测试,至少对我来说是这样的,或者你可能会从使用MSTest的花哨的[AssemblyInitialize]
/ [AssemblyCleanup]
方法中受益,这样你就可以管理一个Dispatcher
用于该项目中的所有测试(这是我们已经完成的)。
一旦超过这些,您可能还会发现在测试中提取mwv.tb_test.Text
也需要在Dispatcher
线程上进行。
你也冒着竞争条件的风险,因为RaiseEvent
引发的事件(取决于你如何处理线程问题)在TextChanged
处理程序获得之前终止在您的测试中连线,这意味着即使在其他所有事情之后,ManualResetEvent
有时也可能永远阻止。
答案 1 :(得分:0)
如Joe的回答所述,您需要在线程上运行调度程序才能使其正常工作。
有关应该运行的单元测试中的代码,请参阅此答案:Task.ContinueWith and DispatcherSynchronizationContext