QA Wolf 上线 Windows 桌面测试 用一次性 VM 解决并行化难题
Running Windows desktop tests from Linux with disposable VMs
QA Wolf 上线 Windows 桌面自动化测试能力,把 Windows VM 建模为一次性设备,由 Linux runner 通过 CDP、Appium 等通道远程驱动原生应用。
QA Wolf 把一次性 Windows VM 抽象为可调度设备复用其移动端调度器,并通过暖池吸收冷启动,为桌面测试基础设施提供了一种可借鉴的架构。
测试 Windows 桌面应用之所以是用户最希望拥有的功能之一,有一个充分的理由:世界上没有人拥有能够把每月数千次测试并行运行的基础设施。
Web 测试基础设施相对容易搭建:无头浏览器在 Linux 容器内几秒钟即可启动,运行成本不到一美分,测试结束后就会被丢弃。而测试 Windows 桌面应用并无对等的方案。
在测试 Windows 应用时:
- 每次测试都需要一台完整的虚拟机,因为 Windows 容器并不包含运行应用所需的图形桌面。
- 机器启动缓慢。冷启动一台 Windows 虚拟机需要 2 到 3 分钟,而浏览器容器只需几秒钟。
- 必须保持登录状态。Windows UI 自动化(UIA)是测试工具用来查找按钮和输入框的可访问性接口,它只能在已激活、已登录的桌面会话内工作,仅仅开机是不够的。
- 机器会记住东西。已安装的文件、注册表项、用户配置文件、保存的登录信息以及残留进程都会在多次运行之间留存下来。
- 每个应用都自带一套“小世界”。安装程序、管理员授权提示、付费依赖、首次运行对话框、登录流程等等。
- 没有内建的可观测性。浏览器会为你提供追踪、日志和截图,而远程 Windows 机器在你自行搭建之前不会提供任何东西。
- 工具链以 Linux 为前提。Kubernetes、CI 运行器以及大多数测试基础设施都是围绕 Linux 容器设计的。
我们的答案是:不再把 Windows 机器当作服务器,而是像手机一样对待它——一台被某次测试独占、用一次后就丢弃的设备。
架构
基本架构借鉴了我们的移动测试基础设施,因为移动测试同样面临这些约束:测试代码无法在被测设备上运行,且每次测试运行时都需要独占一台设备。由于在 Kubernetes 中 Windows 机器被建模为一种设备,测试可以向我们自研的设备调度器请求一台带有 DeviceClaim 的虚拟机,取回连接信息,通过 WebdriverIO 和 Appium 远程操控该设备,并在测试完成后释放机器。
Windows 需要对设备模型进行两项扩展:设备不再必须位于 Kubernetes 集群内部(GCE 虚拟机完全位于集群之外),并且一次申请(claim)可以返回桌面的连接地址,包括用于浏览器的 CDP。除此之外,Windows 运行器在生命周期上完全照搬移动运行器的步骤(申请、心跳、录制、连接、运行、释放),测试通过同一套 WebdriverIO 与 Appium 配对与原生应用通信,专用的 Windows 设备池的工作方式也与专用 iOS 设备相同。
与移动系统一样,它包含三个部分:
- 控制器,运行在 Kubernetes 中。它在 Google Compute Engine(GCE)上维护一个 Windows 虚拟机池,并将虚拟机分配给测试。
- 运行器,一个运行测试代码的 Linux Pod。
- Windows 虚拟机,运行被测应用、自动化工具以及一个轻量级桌面代理。
运行器通过三条连接与虚拟机通信。对于浏览器内的任何操作,Playwright 通过 CDP(Chrome 开发者工具协议)进行连接;原生 Windows 应用由 Appium 处理;WebRTC(网页实时通信)流用于传输屏幕画面,方便人们实时观看测试过程并在事后回看录像。
像 Android 模拟器一样,每台 Windows 虚拟机都是一次性的。没有人登录它、打补丁或让它连续运行数周。一个测试认领它,使用一次后再归还,然后由一台全新的虚拟机取代它的位置。
Linux runner pod 运行测试代码(@qawolf/flows/windows)。第 1 步:测试从设备调度器认领一台设备,该调度器在 Kubernetes 控制器中通过 Windows VM 协调器运行。第 2 步:调度器将 Google Compute Engine (GCE) 上的一台就绪 Windows 虚拟机分配给该测试。调度器还通过 GCE API 维护一个随时可用的 Windows 虚拟机热备池。随后,测试通过三个连接与所认领的虚拟机通信:通过 CDP (Playwright) 连接浏览器,通过 Appium (WebDriver) 连接 Appium 和 WinAppDriver,它们借助 UI 自动化 (UIA) 驱动被测应用。虚拟机上的一个 Go 桌面代理通过 WebRTC 将屏幕流传回 runner。
因此,当用户启动一次运行时:
- 测试在 Linux runner pod 中启动,它会请求一台 Windows 设备。
- 调度器从一组已开机并已登录的机器中为其分配一台虚拟机。
- runner 收到该虚拟机的 CDP、Appium 和屏幕流的连接地址。
- 被测应用被传送到该虚拟机并启动。
- 测试运行。屏幕被实时串流并被录制。
- 测试结束,认领被释放,虚拟机被销毁。一台全新的虚拟机在池中取代它的位置。
第 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 释放认领,调度器销毁该虚拟机并为热备池创建一台全新的虚拟机。
按需租用 Windows 机器
Windows 测试做的第一件事是请求一台 Windows 机器。分发这些机器是我们的设备调度器的工作,它就是那个已经负责分发 Android 模拟器的同一内部服务。针对 Windows,我们让它承担两项新任务:维护从 Google Compute Engine 租来的虚拟机库存,以及为每个提出请求的测试提供一台。
调度器通过三个新的 Kubernetes 自定义资源来跟踪整个设备集群:
WindowsVMTemplate描述一台虚拟机的规格:机器映像、大小和配置。WindowsVMPool描述为给定模板保留多少台就绪的虚拟机。WindowsVM代表一台机器。
设备调度器已经通过这样的记录来管理模拟器,因此 Windows 虚拟机无需另建一套系统即可融入其中。这些记录还使整个集群的状态持久且可见。如果调度器重启,它会读取这些记录并从上次中断的地方继续,工程师也可以使用标准的 Kubernetes 工具检查任何池或虚拟机。
分发一台机器从 DeviceClaim 开始,这是测试用来认领模拟器的同一对象。调度器将认领与相应池中一台就绪的虚拟机进行匹配,并把该虚拟机的连接地址返回给 runner:用于浏览器的 CDP、用于原生应用的 Appium,以及屏幕流。
机器保持可用状态是调度器中协调器的工作。它持续将记录与 GCE 中实际运行的情况进行比对,并弥合差距。如果某个池缺少三台机器,它就创建三台。当测试释放其占用时,它会删除该虚拟机。Windows 虚拟机需要数分钟来完成创建和启动,因此协调器在每次短周期中将每台虚拟机向前推进一步,并把进度写入该虚拟机的记录。缓慢的启动永远不会拖慢调度器管理的其他任务。
快速提供干净的虚拟机
桌面测试基础设施的核心矛盾在于:干净的机器速度慢,而快速的机器又不干净。从全新镜像重启要耗费数分钟,而复用机器意味着每个测试都会继承上一个测试留下的所有痕迹。
我们的系统兼顾了这两方面:
- 每台虚拟机都是一次性使用的。虚拟机在一次测试后绝不会被重复使用。没有任何测试能看到其他测试的文件、设置或登录信息,因此"单独运行通过、在套件中却失败"这类问题根本不会发生。
- 暖池吸收了启动时间。每个池都让虚拟机保持已启动、已登录的等待状态,因此测试领取到的是一台已准备就绪的机器,省去了 2 到 3 分钟的冷启动时间。池中机器在被测试消耗后会得到补充。
- 被遗弃的机器会被回收。被领取的虚拟机在测试运行期间会上报活动状态。如果静默超过 60 秒,控制器就认定该测试已不在,并回收这台机器。
- 容量不足时在可用区和区域间调度。如果某个云可用区无法提供 Windows 虚拟机,池会在另一个可用区中创建。
- 更新以自然淘汰的方式发生。由于虚拟机是一次性的,新镜像无需任何升级流程。旧虚拟机被消耗完毕后会被新机器取代,闲置在旧镜像上的虚拟机也会被回收,于是池会自然而然地完成过渡。
让虚拟机变为一次性使用,省去了维护 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(Model Context Protocol)服务器和公共 API 进行截图、操作和元素检查,使 AI 代理能够以与浏览器运行相同的方式处理 Windows 运行。
保持客户网络封闭
桌面应用经常需要与防火墙后的系统通信:内部 API、授权服务器、数据库等。客户需要准确知道是什么正在与他们建立连接。
- 一个已知的 IP 范围。虚拟机流量来自 QA Wolf 自己的 IP 段,因此客户只需将一个 IP 段加入白名单,而无需维护一组不断变化的云地址。
- 私有池保持私有。专用池只能由其所属的工作区认领。
- 不存储云密钥。控制器通过 Workload Identity 向 Google Cloud 进行身份验证,因此机器上不会存在长期凭证。
- 不留下任何数据。由于每次测试后虚拟机都会被销毁,运行结束后机器上不会保留任何客户数据。
立即将 Windows 添加到您的 E2E 测试套件中
Windows 桌面应用测试现已上线。它通过 QA Wolf 平台和我们的 MCP 运行,使用单一 API 为 Web、iOS 和 Android 编写测试。一个测试可以用 Playwright 打开浏览器,通过 WebdriverIO 和 Appium 控制原生应用,并通过一个 expect 检查结果,无论应用运行在 Web、移动端还是 Windows 桌面上。
来源:Hacker News · qawolf.com