简介这是一份基于C# UI Automation的Windows桌面应用自动化测试示例工程面向.NET开发者和测试人员旨在演示如何通过UI Automation库模拟真实用户操作覆盖打开关闭程序、文本编辑、按钮点击、列表展开、控件遍历等典型场景适用于回归测试与持续集成中的界面验证。压缩包共29个文件约71KB主要包含7个C#源码文件、解决方案与工程配置、编译生成的exe/pdb、资源文件及测试日志项目结构精简便于直接打开研读和运行。工程通过15个按钮示例串联常见自动化操作既展示控件定位与事件触发的基本方法也体现测试流程的组织方式可帮助初学者快速搭建自己的UI自动化测试框架。目前已有2502人学习下载适合希望提升Windows应用测试效率的开发者参考实践。1. C# UI自动化测试示例工程给桌面系统补一套可复跑的回归兜底某公司一套老旧的 WinForms 客户端每次发版前测试都得人肉点十几个窗口登录、录单、审核、导出一套下来四十多分钟还总有漏网之鱼。某次上线后线上报了个“下拉框状态没复位”的问题排查半天发现不是代码逻辑错了是回归根本没覆盖到那个角落。这个基于 C# UI Automation 的自动化测试示例工程瞄准的就是这类 Windows 桌面端的 UI 回归场景用微软官方 UIA 框架去定位窗口和控件模拟点击、输入、下拉选择最后自动断言界面状态。适合正在负责桌面端系统测试、想给老项目补一套自动化兜底方案的开发者拿来改改控件路径就能落到自己的被测系统上。2. 工程结构与技术选型一份分三层的解决方案2.1 先看目录三层职责各管一摊示例工程按“底层能力 → 页面对象 → 测试场景”分了三层目录结构如下UiAutomationSample/ ├── src/ │ ├── UiTest.Automation/ │ │ ├── Core/ │ │ │ ├── UiElementFinder.cs // 元素查找按 AutomationId 控件类型 │ │ │ ├── UiActionExecutor.cs // 动作执行点击、输入、选择 │ │ │ ├── WaitHelper.cs // 等待与超时控制 │ │ │ └── UiAssert.cs // 断言与失败截图 │ │ ├── Pages/ │ │ │ ├── LoginPage.cs // 登录窗口的控件封装 │ │ │ └── MainPage.cs // 主窗体的控件封装 │ │ └── UiTest.Automation.csproj │ └── UiTest.Cases/ │ ├── LoginFlowTests.cs // NUnit 测试用例 │ └── OrderFlowTests.cs ├── tools/ │ └── inspect.cmd // 启动 Inspect 工具 └── README.md每一层的依赖方向是单向的Core 不引用测试框架Pages 引用 CoreCases 引用 Pages。这样分层的直接收益是被测系统的控件路径一旦调整需要改动的只有 Pages 目录里面的类测试方法里的业务步骤可以原样保留。这个结构放在示例工程里看起来有点重但真正跑到三五十条用例时它会帮你省掉大量重复的元素定位代码。2.2 技术依赖官方 UIA 库够用不必一上来就引三方包技术选型上这个工程用的是 .NET Framework 4.7.2 加 System.Windows.Automation测试框架选 NUnit 3.x。被测对象是一个 WinForms 写的模拟项目X工程里包含了它的登录窗口和主窗体拿来做自动化演示正好。System.Windows.Automation 是微软官方托管的 UIA 实现不需要额外装 NuGet 包也不需要写 COM 互操作代码直接 using 就能用。FlaUI 是对 UIA2/UIA3 的第三方封装优点是走最新的 UIA3 通道跨进程性能和对虚拟化列表的支持更好但学习和维护成本高一段。我的看法是这类示例工程用官方库足够等哪天被测系统里的 DataGridView 或者虚拟化列表把性能逼到墙角再切 FlaUI 也不迟。配置方面常见做法是建一个 app.config 或者 application.json集中存放被测应用路径、全局超时毫秒数、失败重试次数三个关键参数。核心类的构造函数从配置里读值比把魔法数字散落在各处要容易维护得多。示例里配置长这样configuration appSettings add keyTargetAppPath valueC:\apps\SimulateApp\SimulateApp.exe / add keyGlobalTimeoutMs value5000 / add keyRetryTimes value2 / /appSettings /configuration参数说明TargetAppPath 是启动被测程序的绝对路径GlobalTimeoutMs 是 UiElementFinder 默认的元素查找超时RetryTimes 是失败后自动重跑次数。这三个值基本就是一个小型 UI 测试工程的全部外部变量了。2.3 页面对象的最小实现LoginPage 怎么写页面对象是这套工程里最值得直接抄走的设计。登录页是每个桌面系统最容易作为落地试点的地方最简实现长这样using System.Windows.Automation; using UiTest.Automation.Core; namespace UiTest.Automation.Pages { public class LoginPage { private readonly AutomationElement _window; public LoginPage(AutomationElement window) { _window window; } public AutomationElement UserNameInput UiElementFinder.FindElement(_window, txtUserName, ControlType.Edit); public AutomationElement PasswordInput UiElementFinder.FindElement(_window, txtPassword, ControlType.Edit); public AutomationElement LoginButton UiElementFinder.FindElement(_window, btnLogin, ControlType.Button); public AutomationElement ErrorTip UiElementFinder.FindElement(_window, lblError, ControlType.Text, 1000); } }这里有个设计取舍页面对象的属性返回的是 AutomationElement 而不是直接执行操作。好处是调用方可以按场景决定到底是 Invoke 还是 Select也让页面对象的职责保持在“元素定位”这一层。查找失败时 FindElement 抛 TimeoutException上层用例能在等待条件里捕获并决定是否重试。3. 核心模块拆解一条从元素查找到状态断言的主链路3.1 UiElementFinder带重试与超时的元素定位元素定位是整个 UIA 测试的地基这个类解决三个问题条件组合、超时控制、中间态异常。完整实现如下using System; using System.Diagnostics; using System.Threading; using System.Windows.Automation; namespace UiTest.Automation.Core { /// summary按 AutomationId 和控件类型查找后代元素带超时轮询。/summary public static class UiElementFinder { public static AutomationElement FindElement( AutomationElement root, string automationId, ControlType controlType null, int timeoutMs 5000) { if (root null) throw new ArgumentNullException(nameof(root)); var idCondition new PropertyCondition( AutomationElement.AutomationIdProperty, automationId); var typeCondition (controlType ! null) ? new PropertyCondition(AutomationElement.ControlTypeProperty, controlType) : Condition.TrueCondition; var condition new AndCondition(idCondition, typeCondition); var stopwatch Stopwatch.StartNew(); AutomationElement result null; while (stopwatch.ElapsedMilliseconds timeoutMs) { result root.FindFirst(TreeScope.Descendants, condition); if (result ! null) return result; Thread.Sleep(300); } throw new TimeoutException($未在 {timeoutMs}ms 内找到 AutomationId{automationId} 的元素); } public static AutomationElement TryFindElement( AutomationElement root, string automationId, ControlType controlType null, int timeoutMs 1000) { try { return FindElement(root, automationId, controlType, timeoutMs); } catch (TimeoutException) { return null; } } } }逻辑说明FindFirst 配合 TreeScope.Descendants 会在 root 的所有后代节点里做深度优先搜索这和 Children 只查一层有本质区别。AndCondition 保证 AutomationId 和 ControlType 同时满足避免出现“ID 对上了但找错控件类型”的误命中。轮询间隔 300 毫秒既不会让 CPU 空转又能保证在控件延迟加载的场景里尽快找到目标。参数说明root 一般来自 AutomationElement.FromHandle 返回的窗口根元素automationId 是 Inspect 工具里读到的 AutomationId 属性controlType 可以传 ControlType.Edit、ControlType.Button 等拿不准时传 null 只按 ID 查。TryFindElement 是给等待条件用的变体不抛异常在轮询场景里反复调用更安全。设计上刻意让超时抛 TimeoutException 而不是返回 null是为了让上层用例能明确区分“元素没找到”和“找错了位置”两类失败前者是时序问题后者是定位条件问题。踩过 WebDriver 显式等待的人会习惯这套思路。3.2 UiActionExecutor点击与输入的真实姿势UIA 的交互操作不是简单调用控件方法而是先查询控件支持的 Pattern。按钮不一定支持 Invoke复选框可能是 TogglePattern单选按钮则走 SelectionItemPattern。示例工程里的执行器把所有情况兜了一遍using System; using System.Windows.Automation; namespace UiTest.Automation.Core { public static class UiActionExecutor { public static void Click(AutomationElement element) { if (element null) throw new ArgumentNullException(nameof(element)); if (element.TryGetCurrentPattern(InvokePattern.Pattern, out object invokePattern)) { ((InvokePattern)invokePattern).Invoke(); return; } if (element.TryGetCurrentPattern(SelectionItemPattern.Pattern, out object selectionPattern)) { ((SelectionItemPattern)selectionPattern).Select(); return; } if (element.TryGetCurrentPattern(TogglePattern.Pattern, out object togglePattern)) { var toggle (TogglePattern)togglePattern; if (toggle.Current.ToggleState ! ToggleState.On) toggle.Toggle(); return; } throw new NotSupportedException( $控件无法执行ClickAutomationId{element.Current.AutomationId}); } public static void SetText(AutomationElement element, string text) { if (element null) throw new ArgumentNullException(nameof(element)); if (!element.TryGetCurrentPattern(ValuePattern.Pattern, out object valuePattern)) throw new NotSupportedException(控件不支持SetValue); var value (ValuePattern)valuePattern; if (value.Current.IsReadOnly) throw new InvalidOperationException(控件是只读状态不能输入); value.SetValue(text); } } }逻辑说明Click 方法的执行顺序是 Invoke → Select → Toggle 三级兜底。大多数 Button 控件支持 InvokePatternRadioButton 和 ListItem 支持 SelectionItemPatternCheckBox 和 ToggleButton 支持 TogglePattern。真实项目里这三种控件长得都像“可以点的东西”UIA 给它们的语义却不同所以必须逐个试探。SetText 先检查 ValuePattern 是否存在再检查 IsReadOnly最后才 SetValue这个顺序一旦反过来只读控件会在 SetValue 时抛出难看的 COM 异常排查起来多绕一层。参数说明Click 和 SetText 都接收 AutomationElement调用方从页面对象拿元素后传入即可。两个方法都刻意设计成同步阻塞被测应用界面卡顿时调用线程会跟着等这样测试用例天然有了一个隐式的粗粒度超时边界。3.3 等待策略为什么 WebDriver 那套显式等待要手工实现Web 自动化里有 WebDriverWait 可以开箱即用但桌面 UIA 没有这种东西。示例工程里实现了一个通用的 WaitUntil核心思路是轮询条件函数并捕获中间态异常using System; using System.Threading; namespace UiTest.Automation.Core { public static class WaitHelper { public static void WaitUntil( Funcbool condition, int timeoutMs 5000, int intervalMs 200, string message 条件等待超时) { var deadline Environment.TickCount64 timeoutMs; string lastError null; while (Environment.TickCount64 deadline) { try { if (condition()) return; } catch (Exception ex) { // 控件刚出现在UI树但还没就绪访问属性会抛异常继续轮询 lastError ex.Message; } Thread.Sleep(intervalMs); } throw new TimeoutException( string.IsNullOrEmpty(lastError) ? message : ${message}最后一次异常{lastError}); } } }逻辑说明循环里对 condition 的调用包了 try/catch这是被实际项目逼出来的。UIA 的 UI 树是动态构建的控件刚进入树但还没完全初始化时访问 Current 属性会抛 ElementNotAvailableException。如果不捕获等待逻辑会被一次中间态异常直接打穿。最后抛出的异常信息里带上最后一次底层异常能让定位省一半力气。实际调用场景是登录后等主面板出现WaitHelper.WaitUntil( condition: () UiElementFinder.TryFindElement(root, pnlMain, ControlType.Pane) ! null, timeoutMs: 8000, message: 登录后主面板未出现);参数说明condition 是一个返回布尔值的委托内部每 200 毫秒评估一次timeoutMs 是整体超时intervalMs 是轮询间隔。用在登录跳转这类 23 秒窗口期时8 秒超时是常见做法轮询间隔 200 毫秒能保证“最多晚 200 毫秒发现界面变化”。3.4 UiAssert断言加截图失败留证比失败本身更重要自动化用例失败时最怕的是只看到一行红字不知道当时界面长什么样。UiAssert 类把元素状态断言和失败截图合并在一起using System.Drawing; using System.Windows.Automation; using NUnit.Framework; namespace UiTest.Automation.Core { public static class UiAssert { public static void ElementVisible(AutomationElement element, string message null) { if (element null) throw new AssertionException(message ?? 断言失败元素不存在); if (element.Current.IsOffscreen) throw new AssertionException(message ?? 断言失败元素在屏幕外); if (!element.Current.IsEnabled) throw new AssertionException(message ?? 断言失败元素处于禁用状态); } public static void TextEqual(AutomationElement element, string expected, string message null) { var valuePattern element.GetCurrentPattern(ValuePattern.Pattern) as ValuePattern; var actual valuePattern ! null ? valuePattern.Current.Value : element.Current.Name; Assert.That(actual, Is.EqualTo(expected), message); } public static void Snapshot(AutomationElement root, string savePath) { var bounds root.Current.BoundingRectangle; if (bounds.IsEmpty) return; using var bmp new Bitmap((int)bounds.Width, (int)bounds.Height); using var g Graphics.FromImage(bmp); g.CopyFromScreen((int)bounds.X, (int)bounds.Y, 0, 0, bmp.Size); bmp.Save(savePath, System.Drawing.Imaging.ImageFormat.Png); } } }逻辑说明ElementVisible 的三个检查里IsOffscreen 是最坑的——窗口最小化或者被遮挡时元素其实还在 UI 树里但状态已经是不可见。这种情况必须捕获否则用例可能在“元素没出现在屏幕上”时依然继续往下跑。TextEqual 做了个双通道兼容能取到 ValuePattern 的活控件读 Value取不到的静态文本直接读 Name。Snapshot 通过 BoundingRectangle 拿到窗口位置和尺寸配合 CopyFromScreen 把整个窗口区域存成 PNG挂到 NUnit 的 TestContext 或日志目录里。参数说明Snapshot 的 root 传窗口根元素即可保存路径建议带上时间戳格式类似${TestContext.CurrentContext.Test.Name}_{DateTime.Now:HHmmss}.png避免多用例并发写同一个文件。这段用到 System.Drawing.NET 6 下要额外装 System.Drawing.Common 包.NET Framework 4.7.2 则不需要。3.5 组合出一条完整用例登录、进主界面、断言用户名把前面几个类串起来的完整用例长这样using System; using System.Diagnostics; using System.Windows.Automation; using NUnit.Framework; using UiTest.Automation.Core; using UiTest.Automation.Pages; namespace UiTest.Cases { [TestFixture] public class LoginFlowTests { private Process _app; private AutomationElement _root; [SetUp] public void SetUp() { var appPath AppConfig.TargetAppPath; _app Process.Start(appPath); _app.WaitForInputIdle(5000); _root AutomationElement.FromHandle(_app.MainWindowHandle); } [TearDown] public void TearDown() { if (_app ! null !_app.HasExited) _app.Kill(); } [Test] public void 管理员登录_进入主界面() { var login new LoginPage(_root); UiActionExecutor.SetText(login.UserNameInput, admin); UiActionExecutor.SetText(login.PasswordInput, 123456); UiActionExecutor.Click(login.LoginButton); WaitHelper.WaitUntil( () UiElementFinder.TryFindElement(_root, pnlMain, ControlType.Pane) ! null, timeoutMs: 8000, message: 登录后主面板未出现); var main new MainPage(_root); UiAssert.ElementVisible(main.UserLabel); UiAssert.TextEqual(main.UserLabel, admin); } } }逻辑说明SetUp 里启动被测进程并等待其主窗口就绪TearDown 里强制 Kill保证用例结束后不残留进程——这一步是血泪经验不清理的话第二次跑用例会因为进程实例冲突直接失败。测试方法本身只描述业务意图输入账号密码、点登录、等主面板、断言用户名控件路径完全被页面对象封装掉了。参数说明WaitForInputIdle(5000) 是等待进程进入空闲状态能有效地避免进程还没创建主窗口就继续往后跑的问题MainWindowHandle 在窗口没出现时是 IntPtr.Zero这一步最好单独封装一个连接器方法详见第 4 章。4. 运行与调试一个用例从编译到跑通的全流程4.1 环境准备三步走环境上只需要 Windows 10 或 11、Visual Studio 2022 和 .NET Framework 4.7.2 开发包。打开解决方案后先还原 NuGet 包再把 app.config 里的 TargetAppPath 改成被测程序的实际路径编译一次确认没有引用缺失基本就可以跑了。有一点预先提醒被测应用是 32 位还是 64 位会直接影响 UIA 的跨进程行为。一个 64 位的测试工程去操作 32 位的 WinForms 程序有可能出现句柄拿到了但后代元素遍历为空的情况。遇到这种翻车现场先把测试工程的平台目标改成 x86多数时候问题直接消失。这属于 UIA 的常见坑后面避坑章节会展开。编译完成后的运行入口是 NUnit可以在 VS 的测试资源管理器里直接跑也可以用命令行dotnet test UiTest.Cases/UiTest.Cases.csproj --filter FullyQualifiedName~LoginFlowdotnet test 的好处是能直接接 CI输出标准化的 trx 结果文件。4.2 连接被测应用主窗口句柄是根锚点所有 UIA 操作都要有一个根元素最常见的锚点是从进程对象拿主窗口句柄public static AutomationElement Connect(Process process) { // 血泪经验主窗口句柄不是永远非零必须等进程把窗口创建出来 process.WaitForInputIdle(8000); if (process.MainWindowHandle IntPtr.Zero) throw new InvalidOperationException(目标进程没有可见主窗口); return AutomationElement.FromHandle(process.MainWindowHandle); }逻辑说明AutomationElement.FromHandle 会把 Win32 窗口句柄包装成 UIA 的根元素后续所有 FindFirst 都在这个根下面进行。WaitForInputIdle 是专门治“进程启动但窗口还没画出来”的启动动画复杂的老系统尤其需要这个等待。参数说明Connect 接收 Process 对象返回 AutomationElement。如果被测程序在测试前已经启动可以用 Process.GetProcessByName 按进程名拿对象再 Connect。还有一种做法是直接传窗口标题用 AutomationElement.RootElement.FindFirst 去找窗口节点适合启动方式比较特殊的场景。4.3 Inspect 工具先定位属性再写代码工程 tools 目录里放了一个 inspect.cmd 启动脚本底层调用 Windows SDK 自带的 Inspect.exe。用它的目的只有一个拿到控件的 AutomationId、ControlType、Name 和 BoundingRectangle。使用顺序是这样的先启动被测程序再用 Inspect 的鼠标跟随模式把焦点移到目标控件上左侧属性栏会实时显示所有 UIA 属性。记录最关键的三项——AutomationId、ControlType、Name其中 Name 只起辅助验证作用因为 Name 可能跟随运行时文案动态变化而 AutomationId 是设计期就定死的。中文系统上 Name 可能是中文或空字符串所以写页面对象时优先 AutomationId。把常用属性整理成一张速查表写代码时直接对照Inspect 属性用途对应代码写法AutomationId元素唯一标识最稳定new PropertyCondition(AutomationElement.AutomationIdProperty, txtUserName)ControlType区分控件种类new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Edit)Name辅助校验可能变化只作为附加条件或断言用BoundingRectangle元素位置尺寸截图定位用一般不进定位条件定位时优先只用 AutomationId 加 ControlTypeName 容易因为国际化文案差异导致用例在不同语言环境下失灵。4.4 断点调试与慢动作模式UIA 自动化调试的难点在于代码执行速度太快界面刚弹出就切换走了人眼根本来不及观察。我一般会在关键步骤之间插入一个带日志的延时器public static void SleepLog(string message, int ms 500) { TestContext.Progress.WriteLine($[{DateTime.Now:HH:mm:ss.fff}] {message}); Thread.Sleep(ms); }逻辑说明TestContext.Progress 是 NUnit 的输出通道比 Console.WriteLine 更可靠因为 VS 测试资源管理器会吞掉控制台输出。每步 500 毫秒的停顿足够肉眼确认界面状态也让 UIA 树有充足时间完成异步刷新。调试模式下我会把这个 SleepLog 插到 SetText 前后、Click 之后、断言之前三到四个点就够。跑通过一轮之后再把延时全部去掉或者调成 50 毫秒以下避免无谓拖慢回归时间。失败场景排查时先看两样东西一是断言抛出的异常信息里带没带“最后一次异常”的底层描述二是失败时自动保存的窗口截图。这两个信息合在一起九成问题能直接定位到“控件没加载出来”还是“控件加载了但状态不对”。5. 避坑笔记UIA 测试里最让人返工的五个场景5.1 找不到控件AutomationId 明明存在却返回 null现象Inspect 里能看到元素的 AutomationId 和 Name同一进程里调用 FindFirst 却返回 null或者间歇性为空。原因最常见的是元素还没进 UI 树窗口在加载动画期间控件尚未创建完成另一个隐蔽原因是窗口根元素绑定错了进程比如 TearDown 清理不干净导致拿到的是上一个残留进程的句柄。解决把裸的 FindElement 换成 WaitHelper.WaitUntil 轮询调用 TryFindElement超时放宽到 810 秒。如果轮询 20 次依然失败返回 root.Current.ProcessId 和当前进程比对一下确认锚点没有串进程。5.2 Invoke 点击没反应但也不报错现象Click 执行后按钮看上去被按下但业务逻辑没触发用例后续步骤全部超时。原因窗口处于非激活状态时部分 WinForms 控件不响应 UIA 的 Invoke 消息它们依赖窗口激活态来同步状态还有一种情况是目标控件其实是自绘菜单或列表项InvokePattern 只是形式上有内部实现是空的。解决点击前先调用 element.SetFocus()让控件拿到键盘焦点再执行 Invoke。还不行就把 Click 实现改成先取 BoundingRectangle 中心点用 Win32 的 SetCursorPos 加 mouse_event 模拟真实鼠标点击。真实鼠标是这类“半残控件”的兜底方案但有副作用——测试执行期间不能再动鼠标否则会点错位置。5.3 远程桌面断开或锁屏之后所有元素消失现象前一天用例还能跑第二天早上发现全部 FindElement 超时窗口截图全黑。原因UIA UI 树依赖一个激活且有会话的桌面。远程桌面会话断开后该会话的桌面进入不可交互状态UIA 拿不到任何窗口元素。这在自动化团队里属于典型的玄学问题——代码没变环境变了。解决把测试跑在一台不锁屏的实体机或服务器虚拟桌面里会话长期保活系统锁屏策略单独豁免这台机器。如果业务上必须用远程机器执行那只能接受锁屏后失败的现状在 CI 里把执行机做常驻会话管理。这个坑没有代码级解法只能从环境上规避。5.4 64 位测试工程找不到 32 位程序的窗口现象从 64 位测试工程启动 32 位 WinForms 程序MainWindowHandle 拿到非零但 FromHandle 之后 FindDescendants 结果为空或者只找到部分元素。原因UIA 跨进程通信在 32/64 位边界上有桥接限制64 位调用方枚举 32 位进程的 UI 树时会丢节点。解决测试工程编译平台改成 x86重新编译后跑同一套用例。改一次 VS 的配置平台即可解决方案配置里新增 x86所有测试项目都选这个平台。如果改了 x86 仍然失败再考虑切 FlaUI 的 UIA3 通道它针对跨进程管道做了更多优化。5.5 自定义控件根本没暴露任何属性现象第三方图表控件、自绘 DataGridView 在 Inspect 里显示为一片空白AutomationId 全是问号手工点也无法定位。原因控件绘制时没有实现 UIA 的 providerUIA 拿不到控件内部结构只能裸露出一个图形容器。解决正规做法是让研发给控件实现 IRawElementProvider 或者继承 WPF 的 AutomationPeer把关键属性暴露出来这一步要排进开发迭代。测试软件侧的替身方案是用 BoundingRectangle 加屏幕坐标来做坐标点击比如把 FindElement 替换成“按 Name 找到外层容器取矩形中心点再偏移固定像素点击”。这个方案极易翻车窗口尺寸一变就要重新调参只能作为最后手段。6. 二次开发路线再往前一步就是一套可维护的 UI 回归框架6.1 页面对象补全 数据驱动示例工程只演示了登录和主面板两条链路。真正落到自己的系统时先把所有高频回归窗口都建成页面对象类每个类只放元素属性不放业务逻辑。然后给登录这类多账号场景加数据驱动[TestCase(admin, 123456, 操作员)] [TestCase(boss, 888888, 管理员)] public void 多账号登录_应进入对应主界面(string user, string pwd, string expectedRole) { var login new LoginPage(_root); UiActionExecutor.SetText(login.UserNameInput, user); UiActionExecutor.SetText(login.PasswordInput, pwd); UiActionExecutor.Click(login.LoginButton); WaitHelper.WaitUntil( () UiElementFinder.TryFindElement(_root, pnlMain, ControlType.Pane) ! null, timeoutMs: 8000); var main new MainPage(_root); UiAssert.TextEqual(main.RoleLabel, expectedRole); }TestCase 的每行参数一式三份账号、密码、预期角色。数据驱动的收益是新增账号场景时不用抄用例方法加一行参数就行。注意被测系统密码加密或验证码逻辑一旦调整这组数据就得同步维护。6.2 接进 CI 流水线 失败自动重试UI 测试天然不稳定一两次偶发失败不能直接判定系统回归。比较成熟的做法是在 CI 里给关键用例设置自动重试两到三次重试仍失败的才触发告警。NUnit 提供了现成的 Retry 属性直接标在测试方法上即可不需要另外造轮子。报告层面我一般把 NUnit 的 trx 结果文件配合 ReportUnit 转成 HTML页面上能直观看到哪些用例在哪个步骤失败。关键用例的失败截图路径写进 TestContext这样打开报告的人不用翻日志就能看到当时的界面现场。示例工程里已经把 UiAssert.Snapshot 挂进断言失败分支CI 里只需要把截图输出目录和报告目录关联起来。6.3 验证方法先跑两条链路再全面铺开不要一上来就想着把所有模块都自动化。我的习惯是先挑两条链路一条是登录进主界面一条是涉及表单提交的回写流程。这两条跑通说明元素定位、等待、断言、进程清理四个基础能力都过关了再往订单、审核、导出这些模块铺开。铺开时严格控制页面对象的增长速度一周内如果新增页面类超过五个就要回头审视是不是页面对象粒度太粗了。从那以后我每次接手桌面端 UI 测试都强制走一遍“Inspect 先读 AutomationId再写页面对象最后才写用例”的流程。这套顺序看着慢实际是最省返工的路属性没确认就写代码等于在赌控件状态等到跑挂再回来查时间已经搭进去一倍。UIA 的坑每个控件都有自己的脾气按流程走稳比快速堆用例重要得多希望帮到你。本文还有配套的精品资源点击获取