如何用一次性 VM 从 Linux 运行 Windows 桌面测试
Running Windows desktop tests from Linux with disposable VMs
为在 Linux 上测试 Windows 桌面应用,该方案把 Windows 机器当作手机式一次性设备:测试通过 DeviceClaim 向设备调度器申请一台已启动并登录的 GCE Windows VM,用 Playwright 走 CDP、Appium 走 WinAppDriver 驱动应用,屏幕经 WebRTC 直播并录制,测试结束即销毁并补充新 VM。
测试 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 虚拟机完全在集群之外),并且认领可以返回桌面的连接地址,包括用于浏览器的 CDP。除此之外,Windows runner 完全按照移动 runner 的生命周期逐步执行(认领、心跳、录制、连接、运行、释放),测试通过相同的 WebdriverIO 和 Appium 组合与原生应用通信,私有 Windows 池的工作方式与私有 iOS 设备相同。
与移动系统一样,它由三部分组成:
- 一个控制器,运行在 Kubernetes 中。它在 Google Compute Engine(GCE)上维护一个 Windows 虚拟机池,并将它们分配给测试。
- 一个 runner,一个运行测试代码的 Linux pod。
- 一台 Windows 虚拟机,运行被测应用、自动化工具和一个小型桌面代理。
runner 通过三种连接与虚拟机通信。Playwright 通过 CDP(Chrome DevTools Protocol)连接,用于浏览器中的任何内容。Appium 处理原生 Windows 应用。WebRTC(Web Real-Time Communication)流传输屏幕画面,以便人们可以实时观看测试并在事后回看录像。
就像 Android 模拟器一样,每个 Windows 虚拟机都是一次性的。没有人登录它、给它打补丁或让它持续运行数周。测试认领它、使用一次后归还,然后由一台全新的虚拟机接替它的位置。
一个 Linux runner pod 运行测试代码(@qawolf/flows/windows)。第 1 步:测试从设备调度器认领一台设备,该调度器在 Kubernetes 控制器中与 Windows VM reconciler 一起运行。第 2 步:调度器将 Google Compute Engine(GCE)上一台就绪的 Windows VM 分配给该测试。调度器还通过 GCE API 维持一个热 Windows VM 池。随后,测试通过三条连接与所认领的 VM 通信:CDP(Playwright)连接浏览器,Appium(WebDriver)连接 Appium 和 WinAppDriver,后者通过 UI Automation(UIA)驱动被测应用。VM 上的 Go 桌面代理将 WebRTC 屏幕流发送回 runner。
那么现在,当用户启动一次运行时:
- 测试在 Linux runner pod 中启动,该 pod 请求一台 Windows 设备。
- 调度器从已启动并登录的机器池中为其分配一台 VM。
- runner 收到该 VM 的 CDP、Appium 和屏幕流连接地址。
- 被测应用被投递到该 VM 并启动。
- 测试运行。屏幕实时流式传输并被录制。
- 测试结束,认领被释放,VM 被销毁。一台全新的 VM 接替它进入池中。
第 1 步,共 6 步测试在 Linux runner pod 中启动,该 pod 请求一台 Windows 设备。
runner pod(测试代码)、设备调度器(Kubernetes)与来自热池的一台 Windows VM 之间的时序。第 1 步:runner 向调度器发送 DeviceClaim。第 2 步:调度器分配一台就绪的 VM。第 3 步:调度器返回该 VM 的 CDP、Appium 和屏幕流连接地址。第 4 步:runner 调用 startApp();VM 通过签名 URL 下载应用、安装并启动它。第 5 步:runner 发送 CDP 和 Appium 命令;VM 通过 WebRTC 将屏幕流式传回,用于实时查看和录制,并向调度器发送心跳;如果静默 60 秒,调度器将回收该 VM。第 6 步:runner 释放认领,调度器销毁该 VM 并为热池创建一台全新的 VM。
按需租用 Windows 机器
Windows 测试要做的第一件事就是请求一台 Windows 机器。分发这些机器是设备调度器的职责,这个内部服务已经在分发 Android 模拟器。对于 Windows,我们给它增加了两项新任务:维护一批从 Google Compute Engine 租用的 VM,并将其中一台分配给每个提出请求的测试。
调度器通过三个新的 Kubernetes 自定义资源来跟踪整个机群:
WindowsVMTemplate描述一台 VM 的形态:机器镜像、规格和配置。WindowsVMPool描述针对给定模板要保留多少台就绪 VM。WindowsVM表示一台机器。
这类记录正是设备调度器管理模拟器的方式,因此 Windows VM 无需单独的系统即可融入其中。它们还使机群状态持久且可见。如果调度器重启,它会读取这些记录并从中断处继续,工程师也可以用标准 Kubernetes 工具检查任何池或 VM。
分发一台机器从 DeviceClaim 开始,这与测试认领模拟器使用的是同一个对象。调度器将认领与来自正确池的一台就绪 VM 匹配,并将该 VM 的连接地址返回给 runner:用于浏览器的 CDP、用于原生应用的 Appium 以及屏幕流。
保持机器库存是调度器内部 reconciler 的职责。它不断将记录与 GCE 中实际运行的机器进行比对,并弥合差距。如果某个池缺少三台机器,它就会创建三台。当测试释放其占用时,它会删除该 VM。Windows VM 的创建和启动需要数分钟,因此 reconciler 会在多次短周期中逐步推进每个 VM,并将其进度写入该 VM 的记录。缓慢的启动永远不会阻碍调度器管理的其他任何任务。
快速提供干净的 VM
桌面测试基础设施的核心矛盾在于:干净的机器慢,而快的机器不干净。从全新镜像重启需要数分钟,而重用一台机器意味着每个测试都会继承上一个测试留下的任何东西。
我们的系统同时处理这两方面:
- 每个 VM 都是一次性的。 测试结束后 VM 绝不会被重用。任何测试都无法看到另一个测试的文件、设置或登录信息,因此一整类“单独运行通过、在套件中失败”的问题永远不会出现。
- 热池吸收启动时间。 每个池都保持 VM 已启动、已登录并处于等待状态,因此测试可以占用一台已经就绪的机器,跳过 2 到 3 分钟的冷启动。池会在测试用尽机器时补充机器。
- 被遗弃的机器会被回收。 被占用的 VM 在其测试运行期间会报告活动。如果它静默 60 秒,控制器会认为测试已消失并回收该机器。
- 容量短缺时迁移区域和地区。 如果某个云区域无法提供 Windows VM,池会在另一个区域创建它。
- 更新通过自然淘汰发生。 由于 VM 是一次性的,新的机器镜像无需升级流程。旧 VM 被用尽并替换为新 VM,而过时镜像上的空闲 VM 会被回收,因此池会自行逐渐迁移。
让机器成为一次性用品,消除了 Windows 机群通常伴随的大量工作:为长期运行的机器打补丁、测试后清理,以及追查某台机器为何与其他机器行为不同。
为自动化而构建的 Windows 镜像
原生应用没有可供 Playwright 驱动的 DOM,因此标准 Windows 安装无法运行自动化 UI 测试。Appium、Appium Windows 驱动和微软的 WinAppDriver 必须在第一个测试到来之前安装、配置并在活动的桌面会话中运行——所有这些我们都烘焙到基础机器镜像中。该镜像还为不属于应用的 Windows 部分(如通知和操作中心)运行一个单独的根级会话。
一个 PowerShell 安装脚本生成该镜像并将其发布到 GCE 中的镜像家族,因此它易于重现。没有人手动维护黄金机器。
基础镜像包含常见软件,如 Google Chrome 和 Adobe Acrobat,这些在 E2E 测试中经常被调用,但我们也支持基础镜像的变体。例如,Office 镜像会在测试看到 Microsoft Word 的首次运行对话框之前将其清除。
当应用需要更多内容时(许可的第三方软件、特定配置或与其一起安装的其他应用),我们可以根据客户提供的安装脚本构建私有镜像。
测试代码留在 Linux 上
运行 QA Wolf 测试的其他所有环节(运行器、录制、CI 集成、结果报告)都构建在 Linux 上。在 Windows 上运行测试代码意味着要为所有这些构建并维护两份副本。所以测试代码不在 Windows 上运行。它运行在 Linux 运行器 Pod 中,并远程控制虚拟机,而虚拟机只运行应用和自动化端点。
当运行器连接到其虚拟机时,它会向同一台机器打开两个通道。虚拟机上的浏览器通过 Playwright 的 chromium.connectOverCDP() 访问,原生应用则通过 WebdriverIO 和 Appium 访问。
测试作者只看到一个 API。@qawolf/flows/windows 入口点为浏览器提供 launch(),为原生应用提供 startApp(),而单个 expect 会根据断言对象使用 Playwright 的匹配器或 WebdriverIO 的匹配器。一个测试可以通过浏览器登录,切换到原生应用并检查结果,而无需关心每一步由哪种协议承载。
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 测试一样,并且通过同一条流水线运行。
将应用送到全新机器上
使用一次性虚拟机时,被测应用必须在每次运行时快速到达新机器,且不能打开安全漏洞。它还常常以安装程序的形式到达,而不是可以直接启动的东西。
应用的文件从不经过运行器。当测试调用 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 构建标签之后。Linux、Windows 和 Mac 代理共享同一代码库,且 Linux 构建不受 Windows 代码影响。
该代理从 Windows 读取实际屏幕尺寸,而不是信任配置,因此流始终以正确的比例显示整个桌面。它还公开诊断和日志端点,报告捕获流水线和 WebRTC 连接的实际运行情况。
对于使用 QA Wolf 的人来说,这归结为:
- 在 Windows 测试运行时实时查看。
- 每次运行的视频录制。
- 用于原生应用的元素选择器,可在编写测试时选取 UIA 元素。
- 通过 QA Wolf MCP(模型上下文协议)服务器和公共 API 进行截图、操作和元素检查,因此 AI 代理可以像处理浏览器运行一样处理 Windows 运行。
保持客户网络封闭
桌面应用经常需要与防火墙后的系统通信:内部 API、许可证服务器、数据库等。客户需要确切知道是什么在连接它们。
- 一个已知的 IP 范围。 VM 流量从 QA Wolf 自己的 IP 范围发出,因此客户只需将一个范围加入允许列表,而无需应对不断变化的云地址集合。
- 私有池保持私有。 专用池只能由其所属的工作区认领。
- 不存储云密钥。 控制器通过 Workload Identity 向 Google Cloud 进行身份验证,因此机器上不会留存长期凭证。
- 无任何持久化。 由于每次测试后 VM 都会被销毁,运行结束后机器上不会保留任何客户数据。
立即将 Windows 加入你的 E2E 测试套件
Windows 桌面应用测试现已上线。它通过 QA Wolf 平台和我们的 MCP 运行,使用单一 API 即可为 Web、iOS 和 Android 编写测试。一个测试可以用 Playwright 打开浏览器,通过 WebdriverIO 和 Appium 控制原生应用,并用一个 expect 检查结果,无论应用运行在 Web、移动端还是 Windows 桌面上。
来源:Hacker News · qawolf.com