HowTo-测试WPF应用-NUnit
HowTo-测试WPF应用-NUnit
问题
- 积累了不少wpf业务控件,都是通过Demo程序测试和调试功能。
- 随着控件的数量和复杂度增加,想通过单元测试来降低复杂度(fix bug后,自动测试bug是否改正,是否引入新bug)
思路
- 查看开源项目如何测试的: ControlzEx
- 细节如下
- 标记测试运行在
STA
- 所有测试之前实例化
SynchronizationContext
,Application
- 新增辅助测试的
TestWindow
DoEvents
- 编写单元测试
- 加载控件到
TestWindow
- 修改控件的状态
- 调用
DoEvents
渲染Ui的修改 - 对修改后的控件状态进行断言
- 加载控件到
- 标记测试运行在
- 所有测试后清理程序
关键代码
标记测试运行在STA
AssemblyInfo.cs
// 程序集的所有测试在STA下运行
[assembly: NUnit.Framework.Apartment(System.Threading.ApartmentState.STA)]
测试环境的设置和清理
AssemblySetup.cs
namespace ControlzEx.Tests
{
using System.Threading;
using System.Windows;
using System.Windows.Threading;
using NUnit.Framework;
// 在指定的命名空间的所有fixture之前执行AssemblySetup.Setup, 之后执行AssemblySetup.TearDown
[SetUpFixture]
public class AssemblySetup
{
[OneTimeSetUp]
public void Setup()
{
// ========知识点==============================
// SynchronizationContext
// 用途: 辅助多线程编程
// 举例: 线程1获取SynchronizationContext.Current传递给线程2,线程2调用SynchronizationContext.Send方法在线程1执行程序
// 详情:
// 只有UI线程才有 SynchronizationContext
// WindowsFormsSynchronizationContext Winform,第一次new Form()自动实例化
// DispatcherSynchronizationContext Wpf,第一次Application.Run()自动实例化
// Application 表示运行中的wpf程序
// OnExplicitShutdown 除非调用 Application.Shutdown(), 否则应用保持运行
// ========知识点==============================
// 手动实例化 SynchronizationContext 用于测试
SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext());
// 手动实例化 不会自动关闭的Application 用于测试
_ = new Application { ShutdownMode = ShutdownMode.OnExplicitShutdown };
}
// Dispatcher 类
// https://docs.microsoft.com/zh-cn/dotnet/api/system.windows.threading.dispatcher?view=windowsdesktop-6.0
// WPF 的 Application.Current.Dispatcher 中,为什么 Current 可能为 null
// https://blog.walterlv.com/post/application-current-may-be-null.html
// Dispatcher.CurrentDispatcher、Application.Current.Dispatcher与DispatcherObject.Dispatcher的区别
// https://www.jianshu.com/p/acdad2533fa8
//
// System.Windows.Threading.Dispatcher 管理线程上的work items的优先级队列的服务
// Dispatcher.CurrentDispatcher
// 获取与当前线程关联的Dispatcher,如果没有就创建一个。
// Application.Current.Dispatcher
// 获取与wpf Ui线程关联的Dispatcher
// Application.Current.Dispatcher.Invoke(onPropertyChangedAction); 后台线程要更新Ui,需要将工作委托给Ui的Dispatcher
// Application.Current 赋值的时间点, 执行Application的实例构造函数
// Application.Current 变为null的时间点, Application.DoShutdown 方法时
// Application.Current 抛NullReferenceException的时间点, 非ui线程调用该值可能抛出该异常(ui线程已经开始关闭,但非ui线程还调用Application.Current)
//
// Dispatcher.InvokeShutdown 关闭线程的work items队列管理服务
[OneTimeTearDown]
public void TearDown()
{
// 清理&关闭当前ui线程中的work items
Dispatcher.CurrentDispatcher.InvokeShutdown();
}
}
}
辅助类
TestWindow.cs
TestWindow.cs
辅助测试的window不要获取焦点,也不需要显示到任务栏
namespace ControlzEx.Tests.TestClasses
{
using System;
using System.Diagnostics;
using System.Windows;
public sealed class TestWindow : Window, IDisposable
{
public TestWindow()
: this(null)
{
}
public TestWindow(object content)
{
this.Content = content;
this.Width = 800;
this.Height = 600;
// 辅助测试的window不要获取焦点,也不需要显示到任务栏
this.ShowActivated = false;
this.ShowInTaskbar = false;
if (Debugger.IsAttached == false)
{
this.Left = int.MinValue;
this.Top = int.MinValue;
}
this.Show();
}
public void Dispose()
{
this.Close();
}
}
}
UITestHelper.cs
UITestHelper.cs
等效于winform的DoEvents方法: Windows Forms 里面的 DoEvents 允许你在执行耗时 UI 操作的过程中插入一段 UI 的渲染过程,使得你的界面看起来并没有停止响应。
namespace ControlzEx.Tests.TestClasses
{
using System.Windows.Threading;
public static class UITestHelper
{
// Dispatcher.PushFrame 方法 Enters an execute loop
// https://docs.microsoft.com/zh-cn/dotnet/api/system.windows.threading.dispatcher.pushframe?redirectedfrom=MSDN&view=windowsdesktop-6.0#System_Windows_Threading_Dispatcher_PushFrame_System_Windows_Threading_DispatcherFrame_
// 深入了解 WPF Dispatcher 的工作原理(PushFrame 部分)
// https://blog.csdn.net/WPwalter/article/details/78093937
// 深入了解 WPF Dispatcher 的工作原理(Invoke/InvokeAsync 部分)
// https://blog.csdn.net/WPwalter/article/details/78093917
// 等效于winform的DoEvents方法: Windows Forms 里面的 DoEvents 允许你在执行耗时 UI 操作的过程中插入一段 UI 的渲染过程,使得你的界面看起来并没有停止响应。、
// 基础知识:
// 调用 Dispatcher.PushFrame 可以在不阻塞 UI 线程的情况下等待。
// 分析:
// 1. 添加了一个 Background(4) 优先级的 DispatcherOperation,执行的操作就是调用 ExitFrame 方法。(如果不明白这句话,请回过头再看看 Invoke/InvokeAsync 这部分 。)
// 2. 调用 Dispatcher.PushFrame 以便在不阻塞 UI 线程的情况下等待。
// 3. 由于用户输入的优先级是 Input(5),UI 响应的优先级是 Loaded(6),渲染的优先级是 Render(7),每一个都比 Background(4)高,于是只要有任何 UI 上的任务,都会先执行,直到没有任务时才会执行 ExiteFrame 方法。(如果不知道为什么,依然请回过头再看看 Invoke/InvokeAsync 这部分 。)
// 4. 当 ExitFrame 被执行时,它会设置 DispatcherFrame.Continue 为 false。
// 4.1 DispatcherFrame.Continue 为 false 的目标是让 Dispatcher.PushFrame(frame); 这一句的等待结束,这样才能继续后面代码的执行。
// 思路:
// 调用 Dispatcher.PushFrame(frame); 来不阻塞地等待;
// 设置 frame.Continue = false 来结束等待,继续执行代码。
/*
public void DoEvents()
{
DispatcherFrame frame = new DispatcherFrame();
Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
new DispatcherOperationCallback(ExitFrame), frame);
Dispatcher.PushFrame(frame);
}
public object ExitFrame(object f)
{
((DispatcherFrame)f).Continue = false;
return null;
}
*/
public static void DoEvents()
{
var frame = new DispatcherFrame();
Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
new DispatcherOperationCallback(
delegate(object f)
{
((DispatcherFrame)f).Continue = false;
return null;
}), frame);
Dispatcher.PushFrame(frame);
}
}
}
具体的单元测试
[TestFixture]
public class TabControlExTests
{
[Test]
public void TestAddRemoveInsertWithItemsSource()
{
var items = new ObservableCollection<string>
{
"1",
"2",
"3"
};
var tabControl = new TabControlEx
{
ItemsSource = items
};
using (new TestWindow(tabControl))
{
var itemsPanel = (Panel)tabControl.GetType().GetField("itemsHolder", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(tabControl);
var tabItemsInitial = GetTabItems(tabControl);
Assert.That(tabItemsInitial, Has.Count.EqualTo(3));
Assert.That(itemsPanel.Children.Count, Is.EqualTo(1));
foreach (var tabItem in tabItemsInitial)
{
tabItem.IsSelected = true;
UITestHelper.DoEvents();
}
Assert.That(itemsPanel.Children.Count, Is.EqualTo(3));
items.RemoveAt(1);
items.Insert(1, "2");
UITestHelper.DoEvents();
Assert.That(itemsPanel.Children.Count, Is.EqualTo(2));
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通