1. 项目概述:为什么是FlaUI?
如果你是一名长期在Windows桌面应用领域耕耘的开发者或测试工程师,提到UI自动化测试,脑子里蹦出来的第一个词很可能是“Selenium”。没错,Selenium在Web自动化测试领域是当之无愧的王者,生态成熟,社区活跃。但当我们把目光转向Windows桌面应用——无论是经典的WinForm,还是现代的WPF——Selenium就显得力不从心了。传统的做法往往是借助Selenium for Windows(即Selenium WebDriver for Windows Desktop)这类工具,或者更底层的Windows API(如UI Automation)配合Coded UI Test(已弃用)或White等框架。这些方案要么配置繁琐,要么对新型控件支持不佳,要么已经停止维护。
这正是FlaUI的价值所在。FlaUI是一个基于微软原生UI Automation技术栈构建的、面向.NET平台的自动化测试库。它直接与应用程序的UI Automation树交互,这意味着它能“理解”WinForm和WPF控件的内在逻辑,而不仅仅是模拟鼠标键盘操作。当你需要测试一个包含复杂数据网格、自定义绘制控件或者深度依赖数据绑定的WPF应用时,FlaUI提供了一种更稳定、更语义化的操作方式。
我最初接触FlaUI,是因为一个遗留的WinForm项目需要构建自动化回归测试套件。当时尝试了基于图像识别和基于消息钩子的方案,要么稳定性差,要么维护成本高。转而使用FlaUI后,最大的感受是:它让测试代码的编写逻辑更接近于开发逻辑。你不再需要去计算一个按钮的屏幕坐标,而是像用户一样,通过控件的名称、自动化ID或者控件类型来定位和操作它。这对于应对UI重构(控件位置变化但功能不变)非常友好。
2. 核心需求解析:桌面应用自动化测试的痛点
在深入FlaUI之前,我们必须先厘清为WinForm/WPF应用做自动化测试到底要解决哪些核心问题。这不仅仅是“点击按钮”那么简单。
2.1 稳定可靠的控件识别与交互
这是最基本也是最核心的需求。桌面应用的UI层次可能非常深,控件可能动态生成,也可能有相同的类名。一个健壮的自动化框架必须提供多种、可组合的定位策略。例如,不仅要能通过Name属性找到“保存”按钮,还要能在某个特定DataGrid的第二行找到“编辑”单元格按钮。Selenium for Windows或早期的UI Automation脚本在这方面常常因为控件句柄变化或属性不唯一而失败。
2.2 对复杂控件和自定义控件的支持
WPF应用大量使用了ListView、DataGrid、TreeView以及各种自定义控件(Custom Control)和用户控件(User Control)。这些控件的内部结构复杂,自动化测试需要能遍历其子项、获取单元格内容、展开树节点等。传统的基于坐标或低级消息的自动化对此束手无策,而FlaUI通过UI Automation可以深入到控件的逻辑树中。
2.3 异步操作与状态等待
桌面应用,尤其是WPF应用,大量使用异步数据绑定和后台线程更新UI。一个常见的场景是:点击“查询”按钮后,需要等待一个DataGrid加载完数据。自动化测试脚本必须有能力智能地等待某个控件进入特定状态(如存在、可见、启用),而不是使用固定的Thread.Sleep,后者是测试脆弱的根源。
2.4 与现有开发流程和CI/CD集成
测试框架需要易于在Visual Studio中编写和调试,能够生成清晰的测试报告,并且可以无缝集成到Azure DevOps、Jenkins等CI/CD流水线中。这意味着它最好能与NUnit、xUnit或MSTest等主流单元测试框架协同工作。
2.5 可维护性与代码可读性
测试代码本身也是代码,需要易于阅读和维护。一个好的框架应该鼓励使用Page Object模式(在桌面测试中常称为Window/Form Object模式)来封装UI交互逻辑,将定位器与操作分离,从而在UI变更时,只需修改少数几个地方。
FlaUI的设计正是围绕解决这些痛点展开的。它并非简单地封装UI Automation API,而是提供了更符合测试人员直觉的、流畅的API接口。
3. 环境准备与项目搭建
让我们开始动手。假设你有一个待测试的WPF或WinForm应用程序(.exe文件)。我们将创建一个独立的测试项目来驱动它。
3.1 创建测试项目
首先,在Visual Studio 2022中新建一个项目。对于测试项目,我推荐使用“NUnit 测试项目”模板,因为它社区活跃,断言库丰富。当然,选择MSTest或xUnit也完全可行,原理相通。
- 打开Visual Studio,选择“创建新项目”。
- 搜索并选择“NUnit 测试项目”,命名为
MyApp.AutomationTests,然后创建。 - 项目创建后,通过NuGet包管理器为该项目安装FlaUI库。打开“工具”->“NuGet包管理器”->“管理解决方案的NuGet程序包”。
- 在“浏览”选项卡中,搜索
FlaUI.UIA3。这里有一个关键选择:FlaUI提供了两种“模式”:- UIA2: 兼容旧的、基于 .NET Framework 的 WinForms/WPF 应用,以及一些MFC应用。
- UIA3: 使用Windows 8及以后版本引入的更新的UI Automation核心,对现代应用(尤其是高DPI、触摸屏)支持更好,是推荐的选择。 对于绝大多数新的WPF和WinForm项目,直接安装
FlaUI.UIA3即可。它会自动引入核心的FlaUI.Core依赖。
注意:如果你的应用是纯Win32(如用C++/MFC编写)或混合了多种技术,可能需要同时引用UIA2和UIA3,并在代码中根据情况切换。但对于标准的.NET WinForm/WPF,UIA3足矣。
3.2 理解核心对象模型
安装完成后,你会在解决方案中看到引用。FlaUI的核心对象模型非常清晰:
Application: 代表被测试的桌面应用程序进程。你可以用它来启动(Launch)、附加(Attach)到已有进程,或者关闭(Close)应用。Automation: 这是入口点,用于获取UIA3Automation实例,它提供了连接UI Automation运行时的基础。UIA3Automation: 具体的自动化实现类,通过它可以获取应用的根元素(GetDesktop())或创建与特定窗口相关的UIA3Automation实例。Window: 代表一个应用程序窗口。这是你大部分操作的起点。FrameworkType: 枚举,标识控件所属的技术框架(如WinForms,WPF,Win32)。FlaUI可以自动识别。
理解这个层次关系很重要:Application->Automation->Window-> 各种Control(如Button,TextBox)。
3.3 编写第一个“Hello World”测试
让我们写一个最简单的测试,启动Windows自带的“记事本”(notepad.exe),在文本框中输入文字,然后关闭。
using FlaUI.Core; using FlaUI.Core.AutomationElements; using FlaUI.UIA3; using NUnit.Framework; using System.Diagnostics; namespace MyApp.AutomationTests { [TestFixture] public class NotepadAutomationTests { private Application _app; private UIA3Automation _automation; [SetUp] public void Setup() { // 启动记事本应用 var appPath = @"C:\Windows\System32\notepad.exe"; _app = Application.Launch(appPath); // 创建UIA3自动化实例 _automation = new UIA3Automation(); } [Test] public void CanTypeTextIntoNotepad() { // 获取记事本的主窗口。这里使用FindFirstDescendant是一种通用方法。 // 更稳健的做法是等待窗口出现,我们稍后会讲。 var mainWindow = _app.GetMainWindow(_automation); Assert.IsNotNull(mainWindow, "应该能找到记事本主窗口。"); // 记事本的编辑区域是一个“Document”控件(在UI Automation中对应Edit控件) // 我们通过控件类型来查找 var textEditor = mainWindow.FindFirstDescendant(cf => cf.ByControlType(FlaUI.Core.Definitions.ControlType.Document))?.AsTextBox(); Assert.IsNotNull(textEditor, "应该能找到文本编辑框。"); // 将文本写入编辑器 string testText = "Hello, FlaUI! 这是一次自动化测试。"; textEditor.Text = testText; // 直接设置Text属性会替换所有内容 // 验证文本是否已输入 Assert.AreEqual(testText, textEditor.Text); } [TearDown] public void TearDown() { // 关闭应用。ForceClose参数确保即使有未保存对话框也关闭。 _app?.Close(); _automation?.Dispose(); } } }运行这个测试,你会看到记事本被打开,输入文字,然后关闭。恭喜,你已经迈出了FlaUI自动化测试的第一步!但这是一个非常基础的例子,真实项目要复杂得多。
4. 控件定位策略:从“找得到”到“找得准”
控件定位是自动化测试的基石。定位不稳定,测试就脆弱。FlaUI提供了强大而灵活的定位机制,核心是使用FindFirstDescendant,FindAllDescendants以及ConditionFactory(通常简写为cf)。
4.1 使用ConditionFactory构建查询条件
ConditionFactory提供了链式方法来构建丰富的查询条件。你可以组合多个条件来精确定位。
// 示例:在一个复杂的WPF窗口中定位一个“保存”按钮 var saveButton = mainWindow.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button) // 是一个按钮 .And(cf.ByName("保存")) // 并且名字是“保存” .And(cf.ByAutomationId("btnSave")) // 并且自动化ID是btnSave(开发时设置的x:Name或Name) .And(cf.ByClassName("ButtonWpfStyle")) // 并且类名包含特定样式(可选) )?.AsButton();ByControlType: 最通用的条件,按控件类型(Button, Edit, DataGrid等)筛选。ByName: 对应控件的Name属性(在WPF中是x:Name,在WinForms中是(Name))。这是最常用的定位方式之一,但要注意名称可能重复或本地化。ByAutomationId:这是最推荐、最稳定的定位方式。它对应WPF中的AutomationProperties.AutomationId或WinForms控件的AccessibleName(并非完全等同,最佳实践是显式设置)。自动化ID在同一个窗口内应该是唯一的。ByClassName: 控件的类名。对于WinForm,可能是WindowsForms10.BUTTON.app.0.141b42a_r9_ad1这种运行时生成的复杂名称,通常不用于定位。对于WPF,是控件的类型名,如Button,TextBox。ByText: 匹配控件的文本内容(如Button的Content,Label的Text)。慎用,因为文本易变。ByProcessId,ByRuntimeId等:更底层的条件,一般用不到。
4.2 处理动态内容和列表控件
对于ListView,DataGrid,TreeView这类控件,你需要先定位到控件本身,然后遍历其子项(Items)。
// 假设有一个WPF DataGrid,AutomationId为“dataGridOrders” var dataGrid = mainWindow.FindFirstDescendant(cf => cf.ByAutomationId("dataGridOrders"))?.AsDataGridView(); // 注意:WPF DataGrid在FlaUI中可能被识别为DataGridView或Table if (dataGrid != null) { // 获取所有行 var rows = dataGrid.Rows; Assert.Greater(rows.Length, 0, "数据网格应该至少有一行数据。"); // 遍历每一行 foreach (var row in rows) { // 获取该行的单元格。可能需要根据列索引或名称来获取。 // 这里假设第一列是“订单号” var cell = row.Cells[0]; // 索引从0开始 var orderNumber = cell.Value; TestContext.WriteLine($"找到订单号: {orderNumber}"); // 你可以在行内查找按钮进行操作,例如“查看详情” var detailBtn = row.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("详情")))?.AsButton(); // detailBtn?.Click(); } }4.3 使用XPath进行高级定位(谨慎使用)
FlaUI也支持通过XPath来定位元素,这提供了极大的灵活性,但XPath通常比原生条件更慢,且依赖于UI Automation树的稳定结构。仅在复杂且其他方法无效时使用。
using FlaUI.Core.Conditions; // ... var element = mainWindow.FindFirstByXPath("//Button[@Name='保存' and @ClassName='Button']");实操心得:定位控件的黄金法则是“自动化ID优先”。在项目开发初期,就和开发团队约定,为所有需要自动化测试的核心交互控件设置唯一的
AutomationId。这能从根本上提升测试脚本的稳定性和可维护性。如果无法控制源码,则优先使用ByName+ByControlType的组合,并考虑控件在可视化树中的相对位置(例如FindFirstChild,FindFirstNested)。
5. 等待与同步:让测试脚本“聪明”起来
在桌面应用中,UI状态的变化往往是异步的。直接操作而不等待,是测试失败的主要原因。FlaUI内置了强大的等待(Retry)机制。
5.1 使用Wait和Retry方法
AutomationElement(所有控件的基类)提供了Wait系列方法。
// 等待一个控件出现(存在于可视化树中) var button = mainWindow.WaitForElement(cf => cf.ByAutomationId("btnSubmit")).AsButton(); // WaitForElement 会等待直到找到元素,超时时间默认为FlaUI的全局超时设置。 // 等待一个控件消失(例如,加载进度条) bool isGone = mainWindow.WaitUntilElementGone(cf => cf.ByAutomationId("progressBar"), TimeSpan.FromSeconds(10)); // 等待控件进入某种特定状态 var textBox = mainWindow.FindFirstDescendant(cf => cf.ByAutomationId("txtInput")).AsTextBox(); textBox.WaitUntilClickable(); // 等待直到可点击(对于TextBox是可获得焦点) textBox.WaitUntilEnabled(); // 等待直到启用5.2 处理自定义等待条件
有时你需要等待更复杂的条件,例如等待DataGrid的行数大于0。
using FlaUI.Core.Tools; // ... // 使用 Retry 类创建一个轮询机制 var dataGrid = mainWindow.FindFirstDescendant(cf => cf.ByAutomationId("dataGridOrders"))?.AsDataGridView(); Assert.IsNotNull(dataGrid); // 等待最多10秒,每隔200毫秒检查一次条件 var success = Retry.WhileFalse( () => dataGrid.Rows.Length > 0, TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(200) ).Result; Assert.IsTrue(success, "在超时时间内,数据网格未能加载出数据行。");5.3 全局超时设置
你可以在测试初始化时设置FlaUI的全局超时和间隔,这会影响所有Wait相关的方法。
[SetUp] public void Setup() { // 设置连接超时(查找应用)、搜索超时(查找元素)等 FlaUI.Core.Tools.Retry.Timeout = TimeSpan.FromSeconds(10); FlaUI.Core.Tools.Retry.Interval = TimeSpan.FromMilliseconds(200); // ... 启动应用等操作 }注意事项:避免在任何测试中使用
Thread.Sleep。它使测试时间不必要地变长,并且无法适应不同性能的机器。始终使用基于条件的等待。一个常见的技巧是,在触发一个预期会改变UI的操作(如点击查询按钮)后,立即使用WaitUntilElementGone等待“加载中”提示消失,然后再去断言或操作结果区域。
6. 模拟用户交互:不仅仅是Click和Type
FlaUI能够模拟几乎所有用户交互,包括鼠标、键盘和触摸(通过鼠标模拟)。
6.1 鼠标操作
var button = mainWindow.FindFirstDescendant(cf => cf.ByAutomationId("btnAction")).AsButton(); // 简单的点击 button.Click(); // 如果需要更复杂的点击,例如双击、右键点击、悬停 button.DoubleClick(); button.RightClick(); // 使用 Mouse 类进行绝对坐标或相对控件坐标的点击(不推荐,除非万不得已) var clickPoint = button.GetClickablePoint(); Mouse.Click(clickPoint); // 拖放操作 (Drag & Drop) var sourceElement = ...; var targetElement = ...; sourceElement.DragTo(targetElement); // 或者使用Mouse类进行更精细的控制 Mouse.Drag(sourceElement.GetClickablePoint(), targetElement.GetClickablePoint(), MouseButton.Left);6.2 键盘操作
// 向焦点控件或特定控件发送按键 var textBox = mainWindow.FindFirstDescendant(cf => cf.ByAutomationId("txtSearch")).AsTextBox(); textBox.Focus(); // 先获取焦点 textBox.Enter("搜索关键词"); // 输入文本,这会模拟逐个字符输入 // 使用 Keyboard 类发送组合键或系统快捷键 Keyboard.Press(FlaUI.Core.Input.KeyboardShortcut.Paste); // Ctrl+V // 或者手动组合 Keyboard.Press(FlaUI.Core.Input.VirtualKeyShort.CONTROL); Keyboard.Press(FlaUI.Core.Input.VirtualKeyShort.KEY_V); Keyboard.Release(FlaUI.Core.Input.VirtualKeyShort.KEY_V); Keyboard.Release(FlaUI.Core.Input.VirtualKeyShort.CONTROL); // 直接设置文本(更快,但不触发所有键盘事件) textBox.Text = "直接设置的文本";6.3 处理模态对话框和非模态窗口
桌面应用经常弹出对话框。FlaUI可以轻松获取并操作这些新窗口。
// 假设点击一个按钮会弹出一个模态对话框 button.Click(); // 方法1:使用Application的GetAllTopLevelWindows(适用于非模态或模态) var allWindows = _app.GetAllTopLevelWindows(_automation); var dialog = allWindows.FirstOrDefault(w => w.Properties.Name == "打开文件")?.AsWindow(); if (dialog != null) { dialog.FindFirstDescendant(cf => cf.ByName("取消"))?.AsButton()?.Click(); } // 方法2:使用主窗口的ModalWindows属性(专门获取模态窗口,更精准) var modalWindows = mainWindow.ModalWindows; if (modalWindows.Length > 0) { var firstModal = modalWindows[0]; // 操作模态对话框... firstModal.FindFirstDescendant(cf => cf.ByName("确定"))?.AsButton()?.Click(); }实操心得:对于文件打开/保存对话框、颜色选择器等系统通用对话框,FlaUI同样可以操作,因为它们也暴露了UI Automation接口。但定位这些对话框的控件可能需要使用
ByClassName(如“#32770”是对话框的常见类名)和ByControlType。建议将这些系统对话框的操作封装成辅助方法。
7. 高级技巧与模式应用
当测试套件变得庞大时,良好的代码组织至关重要。
7.1 实现Page Object模式(Window Object)
将每个窗口或复杂的用户控件封装成一个类,内部包含其控件的定位器和操作方法。测试用例只与这些对象交互,不与FlaUI API直接耦合。
// 示例:登录窗口的Page Object public class LoginWindow { private readonly Window _window; private readonly UIA3Automation _automation; public LoginWindow(Window window, UIA3Automation automation) { _window = window; _automation = automation; } // 控件定位器(作为属性) private TextBox UserNameBox => _window.FindFirstDescendant(cf => cf.ByAutomationId("txtUsername"))?.AsTextBox(); private TextBox PasswordBox => _window.FindFirstDescendant(cf => cf.ByAutomationId("txtPassword"))?.AsTextBox(); private Button LoginButton => _window.FindFirstDescendant(cf => cf.ByAutomationId("btnLogin"))?.AsButton(); private Label ErrorMessageLabel => _window.FindFirstDescendant(cf => cf.ByAutomationId("lblError"))?.AsLabel(); // 操作方法 public void Login(string username, string password) { UserNameBox.Enter(username); PasswordBox.Enter(password); LoginButton.Click(); } public string GetErrorMessage() { // 等待错误信息可能出现 var errorLabel = _window.WaitForElement(cf => cf.ByAutomationId("lblError"), TimeSpan.FromSeconds(3)); return errorLabel?.AsLabel()?.Text ?? string.Empty; } public bool IsLoggedInSuccessfully() { // 检查登录后窗口是否关闭或跳转,这里假设登录成功会关闭本窗口 // 实际可能需要检查主窗口是否出现 return _window.IsOffscreen; // 只是一个示例,并非最佳实践 } } // 在测试中使用 [Test] public void LoginWithInvalidCredentialShowsError() { var mainWindow = _app.GetMainWindow(_automation); var loginWindowObj = new LoginWindow(mainWindow, _automation); loginWindowObj.Login("wrongUser", "wrongPass"); var errorMsg = loginWindowObj.GetErrorMessage(); StringAssert.Contains("用户名或密码错误", errorMsg); }7.2 截图与日志记录
测试失败时,一张截图抵得上千行日志。FlaUI可以方便地对控件或整个屏幕截图。
[Test] public void SomeTest() { try { // ... 测试操作 } catch (Exception ex) { // 测试失败时截图 var screenshot = _app.GetMainWindow(_automation).Capture(); var screenshotPath = Path.Combine(TestContext.CurrentContext.TestDirectory, $"{TestContext.CurrentContext.Test.Name}_{DateTime.Now:yyyyMMddHHmmss}.png"); screenshot.ToFile(screenshotPath); TestContext.AddTestAttachment(screenshotPath); // 将截图附加到NUnit测试报告 TestContext.WriteLine($"测试失败,截图已保存至: {screenshotPath}"); throw; // 重新抛出异常,让测试框架标记为失败 } }7.3 处理进程崩溃和残留
自动化测试可能会触发应用程序的bug导致崩溃。稳定的测试框架需要能处理这种情况。
[TearDown] public void TearDown() { try { // 尝试正常关闭 _app?.Close(); } catch (Exception ex) when (ex is System.InvalidOperationException || ex.Message.Contains("进程已退出")) { // 应用可能已崩溃,忽略关闭异常 TestContext.WriteLine("应用程序在测试结束时已退出或崩溃。"); } finally { // 强制清理:通过进程名杀死所有可能残留的进程 var processName = Path.GetFileNameWithoutExtension(_app?.Name); foreach (var process in Process.GetProcessesByName(processName)) { try { process.Kill(); } catch { /* 忽略 */ } } _automation?.Dispose(); } }8. 常见问题排查与调试技巧
即使准备充分,在实际编写和运行FlaUI测试时,你仍会遇到各种问题。这里记录了一些典型的“坑”和解决方法。
8.1 控件找不到(NullReferenceException)
这是最常见的问题。
- 检查控件属性:使用Inspect.exe(Windows SDK自带)或FlaUInspect(FlaUI官方工具)来查看运行时控件的真实属性。确认你使用的
AutomationId、Name、ClassName是否与工具中显示的一致。注意,WinForms控件的Name属性有时在自动化树中不可见,需要设置AccessibleName或使用Control.SetAutomationId扩展方法(需在项目中引用FlaUI对应平台的扩展包,如FlaUI.UIA3对于WinForms项目)。 - 检查作用域:你是在正确的
Window或父控件下查找吗?模态对话框弹出后,主窗口可能被禁用,此时应在对话框窗口下查找。 - 检查时机:控件是否已经加载完成?在查找前添加足够的等待(
WaitForElement)。 - 框架类型:对于混合应用(如WPF嵌入WinForms控件),可能需要切换
AutomationType(UIA2 vs UIA3)或使用FindFirstNested进行跨框架查找。
8.2 操作无效(如Click没反应)
- 控件状态:控件是否真的
Enabled和Clickable?在操作前使用WaitUntilEnabled()和WaitUntilClickable()。 - 焦点问题:某些操作可能需要控件先获得焦点。尝试在操作前调用
element.Focus()。 - 被遮挡:控件是否被其他窗口或弹出层遮挡?确保测试时屏幕处于活动状态,且没有其他全屏应用干扰。
- 权限问题:以管理员身份运行你的测试运行器(如Visual Studio或命令行),特别是当被测应用也需要管理员权限时。
8.3 测试在CI服务器上失败,但在本地成功
- 交互式桌面:Windows服务或没有登录会话的CI Agent(如运行在SYSTEM账户下的Jenkins Agent)通常没有交互式桌面,无法启动或与GUI应用交互。解决方案:
- 使用专用账户:配置CI Agent使用一个已登录的、有桌面会话的用户账户运行。
- 虚拟显示:在无头服务器上,使用Windows Server的“交互式服务检测”(不推荐且复杂)或通过虚拟机/容器来提供桌面环境。
- 远程桌面保持连接:对于物理机或VM,确保有一个活跃的远程桌面会话,并且锁屏。
- 屏幕分辨率与缩放:CI服务器的屏幕分辨率/缩放比例可能与本地不同,影响基于坐标的操作。永远避免使用基于绝对坐标的操作,坚持使用基于控件属性的逻辑操作。
- 应用启动路径:确保CI服务器上被测应用的路径与本地一致,或使用相对路径、配置文件来管理。
8.4 性能问题
- 过度搜索:避免使用
FindAllDescendants在非常大的窗口(如包含成千上万行数据的网格)中搜索,这非常慢。尽量使用更精确的定位条件,或先定位到最近的容器再搜索。 - 频繁创建Automation实例:
UIA3Automation对象的创建和销毁有一定开销。在测试类或套件级别创建一次,并在所有测试中复用(注意线程安全)。 - 全局超时设置过长:如果测试很多,每个等待都默认10秒,总时间会很长。根据操作类型设置合理的超时:网络操作可以长些(10-30秒),本地UI操作应该短些(3-5秒)。
8.5 调试利器:FlaUInspect
当你的脚本行为不符合预期时,不要盲目修改代码。打开FlaUInspect,连接到你的被测应用,像探索DOM一样浏览整个UI Automation树。你可以实时查看每个控件的所有属性、模式(Patterns)和方法。你可以用它来验证你的定位条件是否正确,甚至可以直接在FlaUInspect中高亮控件、调用方法(如Invoke),这对于理解复杂控件的行为至关重要。
我个人在编写复杂交互的测试时,会同时打开FlaUInspect和Visual Studio,一边操作真实应用,一边观察属性变化,一边编写和调试测试代码,效率非常高。