如何对WPF + TPL进行单元测试

时间:2013-12-21 00:39:53

标签: c# .net wpf unit-testing task-parallel-library

我正在运行一个简单的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本身运行良好。

2 个答案:

答案 0 :(得分:0)

假设window是WPF Windowtb_test是WPF TextBlock

首先,让我先说一下WPF线程模型使得对实时WPF对象运行单元测试变得有点麻烦。就个人而言,我发现这种编码测试的好处是最小的,与处理所有这些的麻烦相比,特别是在遵循MVVM设计模式时。将重要逻辑移动到更适合测试的位置(读取:数据绑定和操作视图模型对象的命令)会使这些测试看起来更加冗余。

如果您可以重新设计您的设计,以便您可以在一个更可测试的位置获得所有“重要”逻辑,那么我会建议尝试。我不知道这个代码库的历史是什么,所以我真的不想留下它。如果你不能,和/或如果你只是好奇,那就让我们走下兔子洞......

在测试中设置当前SynchronizationContext时,您使用的是SynchronizationContext PostThreadPool是使用TaskScheduler.FromCurrentSynchronizationContext实现的(即,回调可以在任何线程上执行)。因此,当TextBlock计划任务延续时,它会在可能是不同的线程上运行“更新Text的{​​{1}}”代码,打破WPF的“必须执行”在Dispatcher线程“规则。

如果Dispatcher.BeginInvoke正在运行,您建议的使用Dispatcher的修补程序可能会解决您的问题。我在发布的测试代码中的任何地方都没有看到Dispatcher.RunDispacher.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