使用一次性虚拟机在 Linux 上运行 Windows 桌面测试
摘要
QA Wolf 介绍其 E2exe 架构:将 Windows 桌面测试建模为可丢弃的「设备」,通过 Kubernetes 设备调度器在 GCE 上分配一次性虚拟机,并结合 Playwright/CDP、Appium 和 WebRTC 实现大规模并行的 Windows 桌面自动化测试。
暂无内容
查看缓存全文
缓存时间:
2026/10/14 16:04
# E2exe:我们如何为企业级规模解决 Windows 桌面自动化测试 | QA Wolf
来源:https://www.qawolf.com/blog/how-we-solved-windows-desktop-automation
测试 Windows 桌面应用之所以是客户呼声最高的功能之一,原因很充分——全世界没有一家公司具备每月并行运行数千次测试的基础设施。
Web 测试的基础设施相对简单:无头浏览器在 Linux 容器中几秒内即可启动,运行成本不到一美分,测试结束后直接丢弃。但测试 Windows 桌面应用却没有对应的方案。
测试 Windows 应用时面临以下挑战:
- **每个测试都需要完整的虚拟机**,因为 Windows 容器不包含可运行应用的图形桌面环境。
- **机器启动速度慢。** 冷启动一台 Windows 虚拟机需要 2 到 3 分钟,而浏览器容器只需几秒。
- **必须有人登录。** Windows UI Automation(UIA)——测试工具用来查找按钮和输入框的无障碍接口——只能在活跃的已登录桌面会话中工作,仅仅开机是不够的。
- **机器会"记住"东西。** 已安装的文件、注册表键值、用户配置文件、已保存的登录凭据和残留进程都会在测试运行之间保留下来。
- **每个应用都有自己的"世界"。** 安装程序、管理员授权提示、许可证依赖、首次运行对话框、登录流程等等。
- **没有内置的可观测性。** 浏览器能提供追踪、日志和截图,但远程 Windows 机器什么都不给你,除非你自建。
- **工具链默认假设是 Linux。** Kubernetes、CI Runner 和大多数测试基础设施都是围绕 Linux 容器设计的。
我们的解决方案是:不再把 Windows 机器当作服务器来对待,而是像手机一样管理——一台设备被一个测试独占、使用一次、然后丢弃。
## 架构设计
基础架构借鉴了我们的移动测试基础设施,因为移动测试面临同样的约束:测试代码无法运行在被测设备上,且每个测试运行时需要独占一台设备。由于 Windows 机器在 Kubernetes 中被建模为设备,测试可以向我们的自定义设备调度器发送一个 `DeviceClaim` 来申请虚拟机,获取其连接信息,通过 WebdriverIO 和 Appium 远程控制设备,测试完成后释放机器。
Windows 对设备模型做了两个扩展:设备不再必须位于 Kubernetes 集群内部(GCE 虚拟机完全在集群之外),以及 claim 可以返回桌面的连接地址,包括浏览器的 CDP 地址。除此之外,Windows Runner 遵循与移动 Runner 完全一致的生命周期流程(claim、heartbeat、record、connect、run、release),测试通过相同的 WebdriverIO 和 Appium 组合与原生应用交互,私有 Windows 设备池的工作方式也与私有 iOS 设备相同。
与移动系统一样,它由三个部分组成:
1. **控制器**,运行在 Kubernetes 中,负责维护一个 Google Compute Engine(GCE)上的 Windows 虚拟机池,并将虚拟机分配给测试。
2. **Runner**,一个运行测试代码的 Linux Pod。
3. **Windows 虚拟机**,运行被测应用、自动化工具和一个小型桌面代理。
Runner 通过三条连接与虚拟机通信。浏览器相关操作通过 CDP(Chrome DevTools Protocol)使用 Playwright 连接,原生 Windows 应用由 Appium 处理,WebRTC(Web 实时通信)流负责传输屏幕画面,供人工实时查看和事后回放录像。
与 Android 模拟器类似,每台 Windows 虚拟机都是一次性的。没有人登录它、为其打补丁或让它连续运行数周。测试申请它、使用一次、然后归还,一台全新的虚拟机会取而代之。
Linux Runner Pod 运行测试代码(`@qawolf/flows/windows`)。第 1 步:测试向设备调度器请求设备,调度器与 Windows 虚拟机调谐器一起运行在 Kubernetes 控制器中。第 2 步:调度器将一台已就绪的 GCE Windows 虚拟机分配给测试。调度器还通过 GCE API 维护一个预热虚拟机池。测试随后通过三条连接与已 claim 的虚拟机通信:通过 CDP(Playwright)连接浏览器,通过 Appium(WebDriver)连接 Appium 和 WinAppDriver,由它们通过 UI Automation(UIA)驱动被测应用。虚拟机上的一个 Go 桌面代理通过 WebRTC 将屏幕流回传给 Runner。
因此,当用户发起一次运行时:
1. 测试在 Linux Runner Pod 中启动,向系统请求一台 Windows 设备。
2. 调度器从已开机并已登录的机器池中分配一台虚拟机。
3. Runner 获取该虚拟机的 CDP、Appium 和屏幕流的连接地址。
4. 被测应用被投递到虚拟机上并启动。
5. 测试运行,屏幕实时流式传输并录制。
6. 测试结束,claim 被释放,虚拟机被销毁,一台全新的虚拟机加入池中。
**第 1 步(共 6 步)** 测试在 Linux Runner Pod 中启动,向系统请求一台 Windows 设备。
Runner Pod(测试代码)、设备调度器(Kubernetes)和来自预热池的 Windows 虚拟机之间的时序流程。第 1 步:Runner 向调度器发送 DeviceClaim。第 2 步:调度器分配一台就绪的虚拟机。第 3 步:调度器返回虚拟机的 CDP、Appium 和屏幕流连接地址。第 4 步:Runner 调用 `startApp()`;虚拟机通过签名 URL 下载应用、安装并启动。第 5 步:Runner 发送 CDP 和 Appium 命令;虚拟机通过 WebRTC 流式传输屏幕用于实时查看和录制,并向调度器发送心跳,如果 60 秒无心跳则被调度器回收。第 6 步:Runner 释放 claim,调度器销毁虚拟机并为预热池创建一台新的。
## 按需租用 Windows 机器
Windows 测试做的第一件事就是申请一台 Windows 机器。分配机器是我们设备调度器的职责——这也是同一个已经在负责分配 Android 模拟器的自研服务。对于 Windows,我们教会了它两项新任务:维护一个从 Google Compute Engine 租来的虚拟机库存,并将虚拟机分配给每个提出请求的测试。
调度器通过三个新的 Kubernetes 自定义资源来追踪整个集群:
- `WindowsVMTemplate` 描述虚拟机的规格:机器镜像、规格大小和配置。
- `WindowsVMPool` 描述某个模板需要维护多少台就绪虚拟机。
- `WindowsVM` 代表一台具体的机器。
设备调度器原本就用类似的记录来管理模拟器,因此 Windows 虚拟机可以无缝集成,无需单独的系统。这些记录还确保了集群状态的持久性和可见性——如果调度器重启,它可以读取记录并从断点继续;工程师也可以使用标准 Kubernetes 工具检查任何虚拟机池或虚拟机。
分配机器从 `DeviceClaim` 开始,这与测试申请模拟器使用的对象相同。调度器将 claim 与正确虚拟机池中的就绪虚拟机匹配,然后将该虚拟机的 CDP(浏览器)、Appium(原生应用)和屏幕流连接地址返回给 Runner。
维护虚拟机库存由调度器内部的调谐器负责。它持续将记录与 GCE 上实际运行的机器进行比对并弥合差距。如果某个池缺少三台机器,就创建三台;当测试释放 claim 时,就删除对应虚拟机。Windows 虚拟机创建和启动需要几分钟,因此调谐器将每台虚拟机的创建过程分成多个短步骤逐次推进,并将进度写入虚拟机的记录中。一次缓慢的启动绝不会阻塞调度器管理的其他任何工作。
## 快速提供干净的虚拟机
桌面测试基础设施的核心矛盾在于:干净的机器速度慢,速度快的机器不干净。从全新镜像重启需要数分钟,而复用机器则意味着每个测试都会继承上一个测试留下的所有状态。
我们的系统同时解决了这两方面:
- **每台虚拟机都是单次使用的。** 虚拟机在测试结束后绝不再次使用。没有任何测试能看到另一个测试的文件、设置或登录凭据,因此"单独运行通过、放进测试套件就失败"这一整类问题根本不会出现。
- **预热池消除了启动等待时间。** 每个虚拟机池都保持虚拟机已启动、已登录、随时待命的状态,测试 claim 到的是一台已经就绪的机器,跳过了 2 到 3 分钟的冷启动。测试消耗掉虚拟机后,池会自动补充新的。
- **被遗弃的机器会被回收。** 被 claim 的虚拟机会在其测试运行期间持续上报活动状态。如果 60 秒无心跳,控制器就认为测试已结束,回收该机器。
- **容量不足时自动跨可用区和区域调配。** 如果某个云可用区无法提供 Windows 虚拟机,池会在另一个可用区中创建。
- **更新通过自然更替完成。** 由于虚拟机是一次性的,新的机器镜像无需升级流程。旧虚拟机被使用完后自动被新虚拟机替换,停留在过时镜像上的空闲虚拟机也会被回收,池会自动逐步完成切换。
将机器设计为一次性使用,消除了通常伴随 Windows 集群管理的大量工作:为长期运行的机器打补丁、测试后清理环境、排查某台机器行为异常的原因。
## 为自动化而生的 Windows 镜像
原生应用没有可供 Playwright 驱动的 DOM,因此原始的 Windows 安装无法运行自动化 UI 测试。Appium、Appium Windows 驱动和微软的 WinAppDriver 必须在第一个测试到来之前就已安装、配置完毕并在活跃的桌面会话中运行——所有这些我们都烘焙进了基础机器镜像。镜像还为不属于被测应用的 Windows 组件(如通知和操作中心)运行一个独立的 root 级会话。
一个 PowerShell 安装脚本用于生成镜像并将其发布到 GCE 的镜像族中,使其易于复现。没有人手工维护一台"黄金机器"。
基础镜像包含 Google Chrome 和 Adobe Acrobat 等常用软件(这些在 E2E 测试中经常被调用),同时我们也支持基础镜像的变体。例如,Office 版镜像会在测试看到之前就清除 Microsoft Word 的首次运行对话框。
当应用的需求超出这些范围时(需要授权的第三方软件、特定配置或需要与其他应用一起安装),我们可以根据客户提供的安装脚本构建私有镜像。
## 测试代码始终运行在 Linux 上
QA Wolf 测试运行的所有其他环节(Runner、录像、CI 集成、结果报告)都构建在 Linux 之上。如果在 Windows 上运行测试代码,就意味着要为所有这些构建和维护两套副本。因此测试代码不在 Windows 上运行——它在 Linux Runner Pod 中运行并远程控制虚拟机,虚拟机上只运行被测应用和自动化端点。
当 Runner 连接到虚拟机时,它通过两条通道连接到同一台机器:通过 Playwright 的 `chromium.connectOverCDP()` 访问虚拟机上的浏览器,通过 WebdriverIO 和 Appium 访问原生应用。
测试作者看到的是统一的 API。`@qawolf/flows/windows` 入口点为浏览器提供 `launch()`,为原生应用提供 `startApp()`,而单个 `expect` 会根据断言对象自动选择使用 Playwright 的匹配器或 WebdriverIO 的匹配器。测试可以通过浏览器登录、切换到原生应用并检查结果,无需关心每一步使用的是哪种协议。
```javascript
const app = await startApp("C:\\qawolf\\apps\\Invoicing.exe");
await app.$("~NewInvoiceButton").click();
await app.$("~CustomerName").setValue("Acme Corp");
await app.$("~SaveButton").click();
await expect(app.$("~StatusBar")).toHaveText("Invoice saved");
```
实际效果是:一个 Windows 测试看起来和 QA Wolf 的任何其他测试一样,并通过相同的流水线运行。
## 将应用部署到全新机器上
使用一次性虚拟机意味着每次运行时被测应用都必须快速投递到一台全新的机器上,且不能留下安全漏洞。此外,应用通常以安装程序的形式到达,而非可直接运行的文件。
应用文件从不经过 Runner。当测试调用 `startApp()` 时,系统会签发一个短期有效的签名 URL,虚拟机通过 Private Google Access(一条全程停留在 Google 网络内部的网络路径)直接从存储中下载应用。只有 QA Wolf 的中央 API 服务才能签发这些 URL。测试代码以非特权用户身份运行,通过受信任的父进程请求下载,因此测试内部的代码无法签发任意 URL。
文件到达后,虚拟机负责安装。它支持 `.exe`、`.msi` 和 `.msix` 安装程序,包括传递 MSI 属性参数,因此仅提供安装程序的应用与独立可执行文件的工作方式完全一致。测试还可以向虚拟机推送文件和拉取文件,覆盖导入文档或检查导出报告等流程。
## 查看远程桌面内部
远程 Windows 虚拟机默认是一个黑盒。编写测试、调试失败或审查运行结果,都需要看到测试当时所看到的画面。
每台虚拟机运行一个用 Go 编写的小型桌面代理。它使用 FFmpeg 捕获屏幕并通过 WebRTC 流式传输,同时通过调用 Windows 自身的 `user32.dll` 来模拟鼠标、键盘和剪贴板输入。该代理不使用 CGo(Go 到 C 代码的桥接),因此可以从 Mac 或 Linux 机器上交叉编译到 Windows,平台特定的部分通过 Go build tag 隔离。Linux、Windows 和 Mac 代理共享同一个代码库,Linux 构建不受 Windows 代码的影响。
代理从 Windows 读取实际屏幕尺寸,而非信任配置文件,因此流式传输的画面始终以正确的比例显示完整的桌面。代理还暴露了诊断和日志端点,报告捕获管道和 WebRTC 连接的实际运行状态。
对于 QA Wolf 的用户来说,这意味着:
- Windows 测试运行时的实时查看。
- 每次运行的视频录制。
- 原生应用的元素选择器,用于在编写测试时选取 UIA 元素。
- 通过 QA Wolf MCP(模型上下文协议)服务器和公开 API 提供截图、操作和元素检查功能,使 AI 智能体可以像处理浏览器测试一样处理 Windows 测试运行。
## 保持客户网络封闭
桌面应用通常需要与防火墙后的系统通信:内部 API、许可证服务器、数据库等。客户需要确切知道是什么在连接他们的系统。
- **单一已知 IP 段。** 虚拟机流量从 QA Wolf 自己的 IP 段发出,客户只需将一个 IP 段加入白名单,而不是面对不断变化的云服务地址。
- **私有池保持私有。** 专属虚拟机池只能被其所属的工作区 claim。
- **不存储云端密钥。** 控制器通过 Workload Identity 向 Google Cloud 进行身份验证,因此不会在任何机器上留下长期凭据。
- **不留存任何数据。** 虚拟机在每次测试后销毁,运行结束后没有任何客户数据残留在机器上。
## 立即为您的 E2E 测试套件添加 Windows 支持
Windows 桌面应用测试现已上线。它通过 QA Wolf 平台和 MCP 运行,使用统一的 API 编写覆盖 Web、iOS 和 Android 的测试。一个测试可以用 Playwright 打开浏览器、通过 WebdriverIO 和 Appium 控制原生应用、并用一个 expect 检查结果——无论应用运行在 Web、移动端还是 Windows 桌面上。
相似文章
X AI KOLs Timeline
Midscene的Computer Agent让桌面UI自动化可以在Linux CI中无头运行,通过xvfb-run自动化,无需真机或VM,支持Electron、Qt、GTK应用。
GitHub Trending (daily)
TesterArmy 开源了 e2e,这是一个 AI 驱动的端到端测试框架。你可以用自然语言描述测试目标,由 Agent 通过 Playwright 或设备模拟器来驱动 Web 或移动端应用;后续运行时操作会被确定性地回放,从而避免额外的模型调用。
X AI KOLs Timeline
团队成员分享了使用AI(DeepSeek V4 Flash)自动创建E2E测试用例并完成开发和调试,一次通过验收的体验,展示了AI辅助开发的潜力。
Hacker News Top
Oxide Computer 介绍了 Voxel(Virtual OXidE Labs)——这是一套在单台机器上模拟完整 Oxide 机架的软件仿真栈,集成了 Propolis 虚拟化、FRR/BGP 网络、SoftNPU P4 交换机以及 SP/RoT 微控制器仿真,让产品保障团队无需使用特殊的测试版本,就能以真实的控制面软件进行高保真度的 CI 测试。
Lobsters Hottest
KDE Linux 已集成 openQA 用于对其系统镜像进行端到端自动化测试,用基于虚拟机的安装和升级验证(通过 AT-SPI 使用 Selenium)取代了基础测试。