
关于
使用 pywinauto 和 Windows UI Automation 对 Windows 原生桌面应用(WPF、WinForms、Win32/MFC、Qt)进行端到端测试。
name: windows-desktop-e2e description: 使用 pywinauto 和 Windows UI Automation 对 Windows 原生桌面应用(WPF、WinForms、Win32/MFC、Qt)进行端到端测试。 origin: ECC
Windows 桌面端到端测试
使用 pywinauto 配合 Windows UI Automation (UIA) 对 Windows 原生桌面应用进行端到端测试。覆盖 WPF、WinForms、Win32/MFC 和 Qt(5.x / 6.x)——Qt 相关指南作为专门章节提供。
何时启用
- 为 Windows 原生桌面应用编写或运行端到端测试
- 从零开始搭建桌面 GUI 测试套件
- 诊断不稳定或失败的桌面自动化测试
- 为现有应用添加可测试性(AutomationId、无障碍名称)
- 将桌面端到端测试集成到 CI/CD 流水线(GitHub Actions
windows-latest)
不适用场景
- Web 应用 → 使用
e2e-testing技能(Playwright) - Electron / CEF / WebView2 应用 → HTML 层需要浏览器自动化,而非 UIA
- 移动应用 → 使用平台特定工具(UIAutomator、XCUITest)
- 不需要运行 GUI 的纯单元测试或集成测试
核心概念
所有 Windows 桌面自动化都依赖于 UI Automation (UIA),这是 Windows 内置的无障碍 API。每个支持的框架都会暴露一棵 UIA 元素树,其属性可供读取和操作:
Your test (Python)
└── pywinauto (UIA backend)
└── Windows UI Automation API ← built into Windows, framework-agnostic
└── App's UIA provider ← each framework ships its own
└── Running .exe
各框架的 UIA 质量:
| 框架 | AutomationId | 可靠性 | 备注 |
|-----------|-------------|-------------|-------|
| WPF | ★★★★★ | 优秀 | x:Name 直接映射为 AutomationId |
| WinForms | ★★★★☆ | 良好 | AccessibleName = AutomationId |
| UWP / WinUI 3 | ★★★★★ | 优秀 | 微软完整支持 |
| Qt 6.x | ★★★★★ | 优秀 | 默认启用无障碍;类名变为 Qt6* |
| Qt 5.15+ | ★★★★☆ | 良好 | 改进的 Accessibility 模块 |
| Qt 5.7–5.14 | ★★★☆☆ | 一般 | 需要 QT_ACCESSIBILITY=1;objectName 需手动设置 |
| Win32 / MFC | ★★★☆☆ | 一般 | 可访问控件 ID;常用文本匹配 |
环境搭建与前置条件
# Python 3.8+, Windows only
pip install pywinauto pytest pytest-html Pillow pytest-timeout
# Optional: screen recording
# Install ffmpeg and add to PATH: https://ffmpeg.org/download.html
验证 UIA 是否可达:
from pywinauto import Desktop
Desktop(backend="uia").windows() # lists all top-level windows
安装 Accessibility Insights for Windows(微软免费工具)——相当于用于检查 UIA 元素树的 DevTools,在编写任何测试之前使用。
可测试性设置(按框架)
在编写测试之前,最有效的做法是为每个交互控件赋予一个稳定的 AutomationId。
WPF
<!-- XAML: x:Name becomes AutomationId automatically -->
<TextBox x:Name="usernameInput" />
<PasswordBox x:Name="passwordInput" />
<Button x:Name="btnLogin" Content="Login" />
<TextBlock x:Name="lblError" />
WinForms
// Set in designer or code
usernameInput.AccessibleName = "usernameInput";
passwordInput.AccessibleName = "passwordInput";
btnLogin.AccessibleName = "btnLogin";
lblError.AccessibleName = "lblError";
Win32 / MFC
// Control resource IDs in .rc file are exposed as AutomationId strings
// IDC_EDIT_USERNAME -> AutomationId "1001"
// Prefer SetWindowText for Name; add IAccessible for richer support
Qt — 参见下方专门章节
页面对象模型
tests/
├── conftest.py # app launch fixture, failure screenshot
├── pytest.ini
├── config.py
├── pages/
│ ├── __init__.py # required for imports
│ ├── base_page.py # locators, wait, screenshot helpers
│ ├── login_page.py
│ └── main_page.py
├── tests/
│ ├── __init__.py
│ ├── test_login.py
│ └── test_main_flow.py
└── artifacts/ # screenshots, videos, logs
base_page.py
import os, time
from pywinauto import Desktop
from config import ACTION_TIMEOUT, ARTIFACT_DIR
class BasePage:
def __init__(self, window):
self.window = window
# --- Locators (priority order) ---
def by_id(self, auto_id, **kw):
"""AutomationId — most stable. Use as first choice."""
return self.window.child_window(auto_id=auto_id, **kw)
def by_name(self, name, **kw):
"""Visible text / accessible name."""
return self.window.child_window(title=name, **kw)
def by_class(self, cls, index=0, **kw):
"""Control class + index — fragile, avoid if possible."""
return self.window.child_window(class_name=cls, found_index=index, **kw)
# --- Waits ---
def wait_visible(self, spec, timeout=ACTION_TIMEOUT):
spec.wait("visible", timeout=timeout)
return spec
def wait_gone(self, spec, timeout=ACTION_TIMEOUT):
spec.wait_not("visible", timeout=timeout)
