如何在 Azure CI/CD 中稳定运行 Windows 11 GUI 自动化测试?
结论:使用专用 VM、自动登录和交互式 agent
这个场景建议采用以下运行方式:
Azure DevOps 流水线
→ GUI 测试专用 agent pool
→ Windows 11 Azure VM
→ 测试账户自动登录,保持桌面解锁
→ 在该账户的交互式会话中运行 agent
→ agent 启动 pytest 和被测应用
Azure DevOps 的 UI 测试文档要求,执行桌面应用 UI 测试的 self-hosted Windows agent 应配置为启用自动登录的交互式进程,配置时不要选择作为 Windows service 运行。参考 Microsoft UI 测试配置。
自动登录负责在重启后创建会话,交互式 agent 让测试进程进入该用户桌面。这些配置不能保证测试永远不会偶发失败,还需要验证显示环境、控件定位、应用状态和恢复流程。
为什么 Windows service 模式会失败?
agent 作为 Windows service 运行时处于 Session 0,而用户通过 RDP 登录后看到的桌面属于另一个会话。即使服务成功启动了应用进程,窗口也不一定会出现在可操作的用户桌面中。
agent 显示在线只能说明它能接收任务,不能证明桌面已解锁、输入可用或显示环境正常。
某些控件接口在锁屏后可能仍能读取属性,但涉及真实鼠标、键盘和焦点的测试不能依赖这种行为。pywinauto 文档列出了锁屏时受影响的操作,包括 click_input()、type_keys() 和 set_focus()。参考 pywinauto 远程执行指南。
第一步:准备专用测试 VM
建议将 GUI 测试与构建任务分开,由流水线把构建产物交给测试 VM 安装或部署。
- 创建专用测试账户,预先完成应用首次启动、许可确认和测试数据准备。
- 固定并记录 Windows、应用、Python 和自动化工具的版本,以及分辨率、DPI、语言和时区。
- 在组织策略允许的范围内,关闭测试期间的自动睡眠、屏幕保护程序锁定和空闲锁屏,并为更新与重启安排独立的时间窗口。
- 测试期间避免人工操作桌面。需要维护时,先停止向该 agent 分配测试。
- 每台 VM 同时只运行一组会争用鼠标、键盘或焦点的测试。需要并行时,增加 VM。
自动登录会让该账户的桌面持续开放,因此应限制 VM、远程访问入口和 agent pool 的访问权限,只运行可信流水线。域策略可能阻止自动登录或禁用屏幕保护程序,采用此架构前需要确认。参考 agent 运行模式说明。
分辨率必须在实际的无人值守状态下验证。RDP 连接期间显示正常,不代表断开后的显示状态也正常。
第二步:配置交互式 Azure DevOps agent
在测试账户下配置 agent:
- 创建专用 agent pool,例如
Windows-UI。 - 下载并解压 Windows agent,执行
config.cmd注册。 - 提示是否作为 service 运行时,选择
No。 - 按配置向导启用自动登录和登录后启动 agent,使用专用测试账户。
- 重启 VM,验证没有人工连接时,账户能够登录,agent 能够上线。
首次排查时,可以在已登录的桌面中执行 run.cmd,确认流水线能启动应用。长期运行应使用配置好的自动启动机制,避免依赖人工保留的命令窗口。具体选项以所安装 agent 的配置向导为准。参考 Windows agent 部署文档。
这里的“后台进程”指用户登录会话中的普通进程。如果使用任务计划程序代替启动机制,也必须让进程运行在已登录用户的交互式会话中;仅设置“无论用户是否登录都运行”不能满足这个要求。
第三步:正确处理 RDP 和 tscon
tscon 可以用于维护完成后的会话交接。Microsoft 的 Azure DevOps 文档给出了将 RDP 会话转交给 console 的做法,但它只能处理已有会话,不能创建登录会话,也不能代替自动登录和故障恢复。
在需要退出的 RDP 会话内,以具备所需权限的 PowerShell 执行:
# 先查看会话,确认当前用户及会话 ID
query user
# 获取当前 PowerShell 所在的会话 ID,避免固定写成 1
$sessionId = (Get-Process -Id $PID).SessionId
& "$env:WINDIR\System32\tscon.exe" $sessionId /dest:console
执行后会断开当前远程连接。执行前应确认 PowerShell 确实位于目标测试账户的 RDP 会话中,不要从 Session 0 的流水线任务里直接套用这段命令。连接其他会话需要相应权限。参考 tscon 命令说明。
这种做法是否足够可靠,需要在这台 Azure VM 上验证。至少要覆盖 VM 重启、无人连接时启动测试、RDP 维护后退出,以及长时间空闲后再次执行。每次交接后都应重新检查分辨率和截图,并执行一次真实 GUI 操作,不能只检查 tscon 是否返回成功。
VNC 是否值得使用?
VNC 可以作为远程维护方式。pywinauto 文档指出,断开 VNC 客户端通常不会破坏活动桌面,但混用 RDP 可能影响这一状态。参考 pywinauto 对 VNC 的说明。
安装 VNC 后,仍要实测具体服务端是否共享测试账户的桌面、断开后是否保持解锁,以及重启后是否恢复。VNC 也不能把运行在 Session 0 的 agent 自动移入用户桌面。
建议先验证自动登录加交互式 agent 的基本架构,再决定是否需要 VNC。若采用 VNC,应限制在受控网络中访问,并验证其认证和加密配置。
第四步:接入 pytest 和 pywinauto
先用控件检查工具确认应用暴露的是 Win32 还是 UI Automation 控件,再选择 backend="win32" 或 backend="uia"。Application 的查找范围受进程边界限制;如果启动器另起进程,需要连接实际 GUI 进程,或使用 Desktop 查找。参考 pywinauto 入门指南。
下面的测试骨架需要按实际应用修改。窗口标题、AutomationId 和预期结果均为占位值,不能直接用于原问题中的应用:
# tests/gui/test_smoke.py
import os
from pywinauto import Application
from pywinauto.timings import wait_until
def test_gui_action():
exe = os.environ["APP_EXE"]
app = Application(backend="uia").start(f'"{exe}"')
try:
window = app.window(title="YOUR_WINDOW_TITLE")
window.wait("visible enabled ready", timeout=30)
window.set_focus()
button = window.child_window(
auto_id="YOUR_BUTTON_ID",
control_type="Button",
)
button.wait("visible enabled", timeout=15)
button.click_input()
result = window.child_window(
auto_id="YOUR_RESULT_ID",
control_type="Text",
)
result.wait("visible", timeout=15)
wait_until(
timeout=15,
retry_interval=0.5,
func=result.window_text,
value="YOUR_EXPECTED_TEXT",
)
finally:
if app.is_process_running():
app.kill()
这个骨架验证一次真实点击及其结果。超时值仅为示例,应按应用响应要求调整。优先等待控件状态或业务结果,避免堆叠固定的 sleep()。示例结束时会终止本次启动的进程;如果涉及未保存数据,应改用应用自身的退出和清理流程。
WinAppDriver 同样需要可用的交互式桌面。切换工具不能修复锁屏或 Session 0 问题;应先解决会话问题,再对目标 Windows 11 环境、应用控件和所选驱动组合做兼容性验证。
第五步:配置 Azure Pipelines
下面假设 Windows-UI pool 中只有符合要求的测试 VM,应用已经安装,固定依赖的 Python 虚拟环境位于 C:\ui-tests\.venv。示例路径需要替换,实际生产流程应在测试前部署本次构建产物。
pool:
name: Windows-UI
steps:
- checkout: self
- powershell: |
$sessionId = (Get-Process -Id $PID).SessionId
Write-Host "SessionId=$sessionId"
if ($sessionId -eq 0) {
throw 'GUI tests cannot run in Session 0.'
}
New-Item -ItemType Directory -Force test-results | Out-Null
& 'C:\ui-tests\.venv\Scripts\python.exe' -m pytest tests/gui --junitxml=test-results/gui.xml
exit $LASTEXITCODE
displayName: Run GUI tests
env:
APP_EXE: 'C:\Apps\YourApp\YourApp.exe'
- task: PublishTestResults@2
condition: always()
inputs:
testResultsFormat: JUnit
testResultsFiles: 'test-results/gui.xml'
failTaskOnFailedTests: true
failTaskOnMissingResultsFile: true
Session ID 检查只能拦截 Session 0,不能判断桌面是否锁定。应在正式用例前增加独立的桌面冒烟测试,验证窗口启动、真实输入及预期结果。会话异常时直接报告环境故障,避免继续产生大量误导性的业务失败。
如何达到可维护的运行状态?
建议把以下能力作为上线条件:
- 每次失败时记录运行账户、Session ID、应用版本和日志,并尽可能在清理进程前保存截图;截图失败也要记录。
- 将截图和诊断日志作为流水线附件或 artifact 单独保存。
- 在用例之间恢复测试数据,处理应用残留进程和异常对话框。
- 桌面检查失败后停止分配任务,通过重启或重新配置恢复,并在恢复后重新验证桌面。
- 记录偶发失败的原因;重试时保留首次失败的证据,不能只留下重试通过的结果。
如果组织策略要求持续锁屏且不允许自动登录,依赖真实键鼠输入的这条无人值守运行路径就不满足条件。此时需要改用允许交互式测试会话的执行环境,或把可下沉的验证移到 API、组件和业务逻辑层。
备注:内容仅供参考。
