拓冰建站拓冰建站
首页 / 资讯中心 / 正文

从Selenium到WinAppDriver:Windows桌面UI自动化框架的设计与实践

简介面向Windows桌面应用自动化测试场景基于Python语言与微软WinAppDriver驱动构建了一套可直接落地的UI测试框架。WinAppDriver兼容Selenium WebDriver协议可驱动UWP与传统Win32桌面应用框架在此基础上封装了测试基类、运行包装器和VNC查看器等模块能够识别按钮、文本框、菜单、列表框等常见控件适合有Python基础的测试工程师或企业级团队快速搭建回归与验收测试体系。压缩包共10个文件以6个Python源码文件为主辅以Markdown说明、Word文档和文本说明除测试用例示例外还包含README、使用说明与附赠资料覆盖环境配置、模块功能解释和脚本写法指引。整套框架从基础封装到具体testcases层均给出示例结构清晰便于按业务扩展控件操作或测试流程。资源整体仅38KB轻量易用已有92人学习下载相比从零构建直接借鉴其分层思路和样例代码可显著降低自动化测试的入门与落地成本。1. 我为什么把一个测试框架从 Selenium 迁移到了 WinAppDriver做过 Windows 桌面应用回归的人大概都有这种经历脚本写了几百条真正维护成本全耗在“今天窗口没起来”“这个按钮被遮挡了”“控件还没加载完脚本就点上去”这三件事上。WinAppDriver 的价值在于它给了 Windows 桌面的 UI 自动化一条和 Web 端几乎一样的路——监听一个本地端口暴露 WebDriver 风格的 JSON 协议让 Selenium 生态里的等待、定位、断言和 CI 流程直接平移过来。这套基于 Python WinAppDriver 的框架把会话管理、元素封装、用例组织都做成了可复制到企业项目的形态适合正在评估桌面端自动化方案或者已经写过一批脚本但被稳定性折磨过的测试开发工程师。2. WinAppDriver 协议桥接与元素识别从 WebDriver 命令到 UI Automation2.1 一次 click 背后串起了哪些组件WinAppDriver 本身不是一个测试框架它是微软提供的一个驱动进程。它在你的机器上启动一个 HTTP 服务监听 4723 端口和 Appium 默认端口一致接收来自 Selenium Python 客户端的命令再把命令翻译成对 Windows UI Automation API 的调用。整个链路是这样的测试脚本调用element.click()Python 的 Selenium 绑定把这条命令封装成 JSON Wire Protocol 请求POST 到http://127.0.0.1:4723/wd/hubWinAppDriver 收到后查找元素对应的 UI Automation 节点调用底层接口完成真实鼠标点击最后把执行结果返回给脚本。这套机制和 ChromeDriver 驱动 Chrome 是同构的这也是为什么企业内部熟悉 Selenium 的人可以零基础接手这个项目——不用重新学一套脚本语言和对象模型。启动 WinAppDriver 是第一步我一般用如下命令C:\Program Files (x86)\Windows Application Driver\WinAppDriver.exe 127.0.0.1 4723 /log C:\logs\winappdriver.log /verbose参数说明第一段是 WinAppDriver 的默认安装路径如果安装在非默认位置需要替换127.0.0.1是监听地址只本机访问时不要改成0.0.0.0避免暴露到局域网4723是端口和脚本里command_executor的地址必须一致/log指定日志输出路径/verbose打开命令级详细日志。这个进程必须以管理员权限启动否则无法读取系统级控件信息问题表现是 session 创建成功后找不到任何元素。2.2 capabilities 参数与 session 建立的完整代码会话创建是整个框架的地基capabilities 配错了后面所有用例行为都不可信。下面这段是从框架的base_testcase.py里抽出来的核心逻辑from selenium import webdriver def create_driver(app_path, attach_hwndNone): caps { app: app_path, platformName: Windows, deviceName: WindowsPC, ms:waitForAppLaunch: 5, } if attach_hwnd: caps[appTopLevelWindow] str(attach_hwnd) driver webdriver.Remote( command_executorhttp://127.0.0.1:4723/wd/hub, desired_capabilitiescaps, ) driver.implicitly_wait(3) return driver逻辑说明app是被测应用的绝对路径WinAppDriver 会在 session 启动时拉起这个进程platformName固定是WindowsdeviceName在 Windows 上是个占位参数写WindowsPC或任意非空字符串都可以ms:waitForAppLaunch是 WinAppDriver 1.2 之后支持的参数控制应用启动后的等待秒数对解决冷启动时控件树未就绪很有效。appTopLevelWindow是另一个思路传入已打开窗口的 HWND 句柄WinAppDriver 会直接 attach 到这个窗口而不是重新拉起进程这在被测应用由安装器或外部进程启动的场景下非常有用。2.3 定位器与 UI Automation 属性的映射关系WinAppDriver 没有自己发明一套全新的定位体系它复用了 Selenium 的By类但底层映射的是 Windows UI Automation 属性。这个映射关系直接决定了你的定位策略选择Selenium 定位器UI Automation 属性获取方式适用场景推荐度By.ACCESSIBILITY_IDAutomationId开发者显式指定或框架推断按钮、输入框、树节点等稳定控件最高语义稳定不随文案变化By.NAMEName控件文本或 Label 关联菜单项、静态文本、无 AutomationId 的控件高但界面文案改动会直接挂By.CLASS_NAMEClassNameWin32 类名或 XAML 控件类型批量处理同一类型的控件列表中容易误匹配By.XPATH属性组合表达式基于 AutomationId、Name、ControlType 组合复杂层级关系定位低控件树大时性能下降明显经验是能拿到 AutomationId 就不碰 Name能用 ID 就不写 XPath。理由很简单Name 依赖界面显示文本产品改一个字你的回归就崩而 AutomationId 是开发在控件上显式打标的变更频率低得多。XPath 在 WinAppDriver 里虽然能用但它是靠遍历整棵控件树来求解的页面控件一多单次定位经常花掉几百毫秒一个用例里几十个定位操作累积延迟非常可观。3. 框架分层设计BaseTestCase 会话基座与 Wrapper 操作封装3.1 BaseTestCase把会话生命周期钉死在基座里企业级框架和 demo 脚本最大的区别是会话的创建和销毁有没有收口。这个项目的base_testcase.py把所有用例共用的 session 初始化、启动参数、关闭逻辑集中在一个基类里继承它的测试类不再关心 WinAppDriver 连不连得上、应用有没有拉起这类琐事。import unittest from selenium import webdriver class BaseTestCase(unittest.TestCase): driver None classmethod def setUpClass(cls): caps { app: rD:\Program Files\RealVNC\VNC Viewer\vncviewer.exe, platformName: Windows, deviceName: WindowsPC, ms:waitForAppLaunch: 5, } cls.driver webdriver.Remote( command_executorhttp://127.0.0.1:4723/wd/hub, desired_capabilitiescaps, ) cls.driver.implicitly_wait(3) classmethod def tearDownClass(cls): if cls.driver: cls.driver.quit()逻辑说明这里用的是unittest.TestCase风格setUpClass和tearDownClass是类级别钩子整个测试类共享一个 driver 实例避免每条用例都重启应用。implicitly_wait(3)设置的是全局兜底超时意思是元素查找最多等 3 秒这个值不是越大越好设大了会让失败用例的暴露时间拖得很长。注意desired_capabilities的传参方式在 Selenium 4.x 里已经标记为 deprecated但 WinAppDriver 官方文档至今仍以这种方式为例因为这个驱动本身对 Options 模型的支持不完整保持 dict 写法是为了兼容性而不是守旧。如果团队用的是 Selenium 4.6也可以把这份 dict 塞进Options只是要自己测一遍升级路径。3.2 test_wrapper让所有元素操作都经过统一出口utils/test_wrapper.py是这个框架里另一个关键文件它把click、input_text、is_visible这些高频动作封装成带等待、带日志的方法。直接操作 Selenium 原生的driver.find_element().click()不是不行但每一条用例里都写find_element会让调试成本和维护成本线性上升而且异常处理逻辑散落在各处。import logging from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class TestWrapper: def __init__(self, driver, wait_timeout10): self.driver driver self.wait_timeout wait_timeout def _wait_element(self, by, value, timeoutNone): timeout timeout or self.wait_timeout return WebDriverWait(self.driver, timeout, poll_frequency0.5).until( EC.presence_of_element_located((by, value)) ) def click(self, by, value, timeoutNone): el self._wait_element(by, value, timeout) logging.info(click [%s] - %s, by, value) el.click() def input_text(self, by, value, text, timeoutNone): el self._wait_element(by, value, timeout) el.clear() el.send_keys(text)逻辑说明_wait_element是内部方法统一走了WebDriverWait的显式等待poll_frequency0.5控制轮询间隔默认 0.5 秒查一次控件比 Selenium 默认的轮询更能及时发现控件出现click方法先等元素出现、记录日志、再执行点击把“定位、等待、操作、留痕”四件事合并到一行调用里。input_text里先clear()再send_keys()避免输入框里残留上一次的数据。这个封装的实际价值在于将来如果要在所有操作前加一个统一的截图钩子或者全局捕获控件不可用异常只需要改这一个文件不需要动几十个用例。3.3 目录结构与文档怎么配合使用解压后看到的运行库结构是下面这样值得照着梳理清楚再动手WinAppUITest-main/ ├── base_testcase.py ├── testcases/ │ └── vnc_viewer.py ├── utils/ │ ├── test_wrapper.py │ └── __init__.py ├── README.md ├── 说明文件.txt ├── 附赠资源.docx └── .gitattributes逻辑说明testcases/放被测应用的用例文件项目自带一个vnc_viewer.py以 VNC Viewer 这个真实 Windows 客户端为对象写了一套可运行的样例utils/放工具类test_wrapper.py是元素操作封装base_testcase.py在根目录而不是 utils 里因为它是所有用例的父类放根目录更容易被 import。.gitattributes保证了仓库文件在 Windows 和 Linux 之间 Checkout/Checkin 时换行符一致避免 CRLF 干扰 diff。配套文档的角色也不同README.md描述安装依赖和框架设计说明文件.txt更像快速上手指引告诉你先跑哪条命令、再看哪个文件附赠资源.docx一般是依赖清单和各模块 API 说明。实际接手时推荐的阅读顺序是先看说明文件.txt再对照 README 跑一遍样例最后按需查 docx 里的 API 细节。4. 控件定位策略与等待参数调试把 VNC Viewer 用例跑稳4.1 三种定位方式在真实控件上的差异拿样例里的 VNC Viewer 来说连接对话框里的服务器地址输入框、连接按钮、认证弹窗都能用不同的定位方式命中。下面这段代码示范了三种写法from selenium.webdriver.common.by import By # 方式一AutomationId addr_input wrapper.click(By.ACCESSIBILITY_ID, AddressInput) connect_btn wrapper.click(By.ACCESSIBILITY_ID, ConnectButton) # 方式二Name dialog_title wrapper._wait_element(By.NAME, VNC Viewer) # 方式三ClassName 配合层级约束 auth_ok_btn wrapper._wait_element(By.CLASS_NAME, Button, timeout8)逻辑说明方式一优先使用AddressInput和ConnectButton是控件上的 AutomationId只要开发不改控件命名就永远稳定方式二用来确认窗口是否弹出靠的是窗口标题文本方式三要小心ClassName匹配的是Button这种类型名页面上可能有多个按钮同时满足条件find_element默认返回第一个匹配项一旦按钮顺序变化就定位错对象。给它的建议是只在确信页面只有一个同类控件时使用或者配合 XPath 加上父容器约束。表格里已经列过推荐度这里再给一条现场经验如果发现某个控件在 Inspect 工具里看到的 AutomationId 是空字符串不要硬找 ID直接用它的 Name或者让开发在代码里补上 AutomationId不要自己写复杂的 XPath 去绕——那是在透支后续维护成本。4.2 WebDriverWait 参数调优与真正需要注意的坑样例用例里大量出现显式等待这是 UI 自动化稳定性最核心的一层。WebDriverWait的默认参数能用但没法应对桌面应用常见的“控件出现了但还没就绪”的场景from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import NoSuchElementException wait WebDriverWait(driver, 15, poll_frequency0.5, ignored_exceptions(NoSuchElementException,)) element wait.until( EC.element_to_be_clickable((By.ACCESSIBILITY_ID, ConnectButton)) )逻辑说明WebDriverWait第一个参数是驱动实例第二个15是最长超时秒数poll_frequency0.5是轮询间隔默认是 0.5 秒也可以调成 0.3 让控件出现后响应更快但会略微增加 CPU 占用ignored_exceptions里的NoSuchElementException意味着元素没找到时继续等而不是立刻抛异常。EC.element_to_be_clickable比presence_of_element_located更严格它要求元素不仅存在而且处于可点击状态。桌面应用和 Web 一个显著差异是窗口可能已经出现但控件还在初始化过程中presence能通过而element_to_be_clickable会一直等到真正可交互这也是稳定性提升的关键。等待相关的参数可以按下面的基准来配参数推荐值说明timeout等待超时10~20 秒取决于应用启动和响应速度冷启动场景给 20poll_frequency轮询间隔0.3~0.5 秒太密会增加 IPC 开销太疏会拖慢用例implicitly_wait全局兜底2~3 秒只做兜底核心交互全部用显式等待ignored_exceptionsNoSuchElementException也可以加ElementNotVisibleException这里有一个常见误用值得单独说不要在一个用例里混用大量time.sleep(2)然后又把implicitly_wait设成 10 秒这样的结果是每条用例都带固定延迟跑完一遍要几十分钟而真正等不到的元素照样等不到。正确思路是固定延迟只加在“已知控件树重建耗时较长”的节点上比如应用切换主界面之后其他全部交给显式等待。4.3 元素找不到时先看现场再改代码框架里最应该复用却不是每个人都会用的是现场保留机制。WinAppDriver 支持把当前控件树 dump 出来也支持截图这两样是排查定位问题的第一手证据。# 定位失败时保存页面 XML 结构和截图 with open(page_source.xml, w, encodingutf-8) as f: f.write(driver.page_source) driver.get_screenshot_as_file(failure.png)逻辑说明driver.page_source返回当前窗口的 UI Automation 树格式是 XML里面能看到每个控件的 AutomationId、Name、ClassName、IsEnabled 等属性截图则记录了真实画面。排查步骤是先打开 page_source.xml搜索用例里定位不到了那个控件的 Name 或 AutomationId看控件到底在不在。如果不在说明窗口没切对或应用没加载到那个页面如果在但找不到说明定位器写错了对照 XML 里的实际属性修正即可。这一步能区分出 80% 的问题是环境问题还是定位问题避免反复改代码盲试。5. 企业级落地技巧失败重试、现场保留与 Windows Runner 集成5.1 给非稳定操作加上重试装饰器桌面应用的 UI 交互比 Web 多了一层窗口系统的不确定性偶发性的点击没生效很难完全避免。给高频但偶发失败的操作套一层重试装饰器是投入产出比最高的稳定性手段。import functools import time def retry(times3, interval1.0): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): for i in range(times): try: return func(*args, **kwargs) except AssertionError: if i times - 1: raise time.sleep(interval * (i 1)) return wrapper return decorator逻辑说明times是重试次数interval是基础重试间隔这里用了递增退避——第一次失败等 1 秒第二次等 2 秒给应用留出恢复时间。装饰器只捕获AssertionError因为 UI 断言失败往往代表控件态不对值得重试而元素找不到这类异常本身已经在等待机制里处理过了不需要再套一层。使用时直接retry(times3, interval1.0)标记到用例方法上即可。5.2 失败现场的两种证据要同时保留截图和 page_source 各有所长截图反映视觉状态page_source 反映控件状态两类证据缺一不可。建议在tearDown里判断测试结果失败时自动执行上一章那两行保存逻辑输出路径带上时间戳和用例名方便 Jenkins 或者 GitLab CI 归档。5.3 Windows Runner 上的落地边界CI 里跑 Windows 桌面 UI 自动化有一个绕不开的约束WinAppDriver 需要访问交互桌面所以 Runner 必须以交互式会话运行。常见的做法是在Start-Process里拉起 WinAppDriver 和被测应用保证它们运行在同一个和桌面相连的会话中而不是挂在某个 Windows 服务进程中。一台 Runner 也不要并行跑多个桌面 UI 任务两个 session 抢同一个桌面会话会互相干扰用独占标签或者队列串行是最省心的策略。日志路径我习惯固定到一个独立目录比如C:\artifacts\wd.log这样每次构建失败后能快速拉回 WinAppDriver 自己的命令级日志配合框架里的 page_source 和截图定位效率会高很多。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门