iT邦幫忙

2026 iThome 鐵人賽

DAY 30
0
Software Development

Windows 桌面軟體 CI 實戰:從系統工具開發到 Zero-RDP 自動化測試錄影系列 第 30 篇

Day 30:從黑箱到確定性,Windows GUI 自動化的三十天工程總結

  • 分享至 

  • xImage
  •  

三十天前,這個系列的第一篇拋出了一個現實問題:為什麼在現代軟體工程中,Windows 桌面應用的自動化測試幾乎被遺忘在 CI 之外?

長年以來,桌面開發團隊普遍接受了一種宿命論:「GUI 測試就是脆弱、flaky、動不動就卡死,在沒有人看顧的 CI 機器上跑不起來;與其花時間修那些幽靈失敗,不如在發版前讓 QA 手動點一遍。」

這 30 天的實測與探索,只為推翻這個宿命。

我們從最底層的 Window Station 與 Desktop 桌面架構出發,走過 PyAV 串流錄影、UIA COM 代理人邊界、WinUI 3 Content Island 焦點賽跑、掃描碼與輸入法吞鍵;經歷了 ARM64 runner 上記事本冷啟動高達近 80 倍的差距、單例應用磁碟狀態殘留、清場腳本差點誤殺 Console Host 的危機;最後走進四個真實開源專案(Notepad++、WinMerge、DB Browser for SQLite、Files),親手抓出四個被上游修復的真實回歸。

這一切證明了一件事:Windows 不是隨機的黑箱,而是嚴格的物理學。

每一個看似偶發的「flaky 測試」、每一個「等了 30 秒沒有新視窗」、每一個「送出按鍵卻毫無反應」,背後都有精確、可追溯且必定發生的系統因果律。當你尊重作業系統的設計模型、不再依賴猜測,Windows GUI 測試在 CI 上的穩定度,完全可以達到與 Web 端 Playwright 同等級的確定性。

在系列的最後一天,我們不列死板的打勾清單,而是將這三十天無數次失敗、量測與推翻假設所換來的心智模型,凝練為四大系統鐵律、一套落地策略,以及這套架構的可為與不可為。


四大系統鐵律

如果這三十天只能留下四條寫在白板上的原則,它們是:

鐵律一:失敗是安靜的,不信任回傳值,只信任可驗證的後置條件

這 30 天裡出現頻率最高、代價最昂貴的陷阱,全部源於「呼叫成功,但什麼都沒發生」:

  • SendInput 回傳成功,但焦點視窗在另一張桌面上。
  • PrintWindow 執行成功,但截出來的畫面是一片全黑。
  • FindFirst 一次性搜尋回傳「找不到」,但目標控制項就活在幾何邊界之內。
  • GetCurrentColumnHeaders() 回傳成功的空集合,但欄位標頭明明掛在畫面上。
  • click() 點擊邊界為 (0, 0, 0, 0) 的元素,沒有拋出任何例外,只留下一片死寂。

Windows 的原生 API 是為呼叫端與接收端協同設計的,不是為自動化審計設計的。 它不會主動警告你「這次操作沒有語意效果」。

因此,自動化測試最核心的紀律只有一條:永遠不要相信任何 API 的直接回傳值,只相信你親自驗證過的後置條件。 不用 sleep 賭它好了沒,而是用輪詢等待明確的狀態改變(_verified 系列);不問「有沒有拋出例外」,而是問「結果是否真正可用」。

鐵律二:沒有現場的 CI 只是在猜謎,可觀測性高於斷言本身

一個沒有診斷產物的 GUI 測試,只要在 CI 上失敗一次,整個團隊就會陷入猜謎:是網路斷了?焦點被搶了?對話框跳出來了?還是系統卡死了?

猜謎的成本極高,幾次之後,團隊通常會做出最糟的妥協:「那個測試好像很 flaky,先 @pytest.mark.skip 掉吧。」

測試之所以變成 flaky,是因為你看不見失敗現場。 第一章建立的四個產物:全程 MP4 錄影、視窗普查(Census)、事件時間軸(Timeline)以及失敗瞬間的切格截圖(frames/),並非除錯的輔助裝飾品,它們是整套自動化工程的飛行記錄器。

當 CI 紅掉的那一刻,工程師不需要連進 runner,也不用重推一個 commit 加 log:

  1. 打開 READ_THIS_FIRST.md 與例外簽名(Signature),一眼看出是哪個外部行程搶走前景。
  2. 點開 frames/,直接看見失敗當下的視窗畫面與焦點狀態。
  3. 對照 session_events.jsonl,精確定位毫秒級的事件順序。

診斷產物是阻斷「猜測與重試」循環的唯一武器。 把失敗現場做得越清晰,測試套件的壽命就越長。

鐵律三:CI 的機器不是你的機器,在最惡劣的環境下驗證契約

我們在開發機上調試時,面對的是一台極度偏袒你的電腦:

  • 記事本早就啟動過,所有 DLL 與 XAML 執行階段快取都熱騰騰地留在記憶體裡。
  • 登入的是互動式 Session 1,有真實螢幕、真實 GPU、真實的 DPI 縮放。
  • 沒有未解的 OOBE 設定頁、沒有 Microsoft Store 在背景掃描換包、也沒有 WSL 正在每隔 30 秒彈出提示視窗。

但在 GitHub Actions 剛開機的拋棄式 runner 上,這一切假設全部瓦解。 我們實測過:Win11 記事本的冷啟動耗時,可以從本機的幾百毫秒暴增至無人機房內的 7.84 秒,在資源緊繃與換包競爭下甚至飆升至 17.18 秒;Windows Server SKU 缺少 Task View 與現代虛擬桌面 COM 介面;終端機 ConPTY 的 Console Host 沒有傳統視窗,輕率殺行程會直接導致整條 pipeline 自毀。

一個合格的桌面測試工程師,在自己的開發機上寫好每一行程式碼時,都必須反問一句:

「在一台剛開機、沒人在看、字型還沒快取、背景隨時有系統視窗搶焦點的拋棄式 VM 上,這行程式碼還能成立嗎?」

鐵律四:量測勝過推論,以證據為唯一的裁判

這 30 天裡,作者本人錯得最離譜的幾次判斷,全部來自「聽起來極度合理的邏輯推論」:

  • 看到 traceback 停在 import av,直覺推論「PyAV 的 ARM64 wheel 壞了」;實測發現是清場腳本誤殺了共用 Console 的 Windows Terminal,行程死於 0.27 秒內的 KeyboardInterrupt。
  • 看到 CI 狀態卡在 in_progress 12 分鐘,推論「程式碼發生了 12 分鐘的死結」;實測發現行程早就死透,只是孤兒行程握著 stdio pipe 不放。
  • 看到 DB Browser 在 ARM64 全紅,推論「模擬架構不穩定」或「Qt 橋接沒起來」;實測 dump 整棵樹,才發現是 x64(Qt5)與 ARM64(Qt6)連同版本二進位檔的 automation_id 設計都完全不同。
  • 看到記事本啟動逾時,直覺推論「x64 轉譯模擬層太慢」;實測比對才發現 Server 上跑的是三十年前的 Win32 純 C 程式,Client 上跑的是重度依賴 Windows App SDK 的現代打包應用。

自動化除錯中最昂貴的浪費,就是用想像力代替測量。 面對無法解釋的現象:先丟 probe、先查真實 PID、先切錄影影格、先比對系統版本與雜湊。現實作業系統的複雜度,永遠超越人類未經量測的推理。


建立新專案的落地路徑

當你準備為一個全新的 Windows 桌面應用建立 CI 時,請依序推進以下四個階段。順序不能顛倒,先看得到現場,再追求穩定,最後才擴充覆蓋率:

階段 核心主題 涵蓋天數 核心目標與關鍵交付物
階段 1 現場可觀測性 Day 3–5 建立最小 Smoke 測試,配置錄影、視窗清冊普查與 if: always() 產物保全
階段 2 環境與生命週期防護 Day 16–20 辨識系統 SKU 差異,關閉系統更新噪音,實作安全清場與動態非同步輪詢
階段 3 穩健定位與語意驅動 Day 6–15 導入視窗清冊 Diff、候選過濾階梯、Control Pattern 與純英文鍵盤配置
階段 4 防禦性驗證與持續整合 Day 21–29 實作 Fail-closed 驗證、矩陣隔離、失敗簽名診斷與 ARM64 本機對照實驗室

階段 1:現場可觀測性(Day 3–5)

核心目標:建立黑箱外的眼睛。在追求任何測試通過率之前,先保證「失敗時看得到現場」。

  • 啟動最小 Smoke 工作:先寫一個只做「啟動行程 ➔ 截一張圖 ➔ 正常退出」的最小工作,驗證 Runner 具備可互動的桌面環境與基礎權限。
  • 建立產物保全流水線:將螢幕錄影(60–120 秒循環緩衝)、視窗清冊普查與時間軸日誌納入工作流程,並無條件以 if: always() 保全上傳 Artifact。現場資料只要丟失一次,該次除錯的時間成本就直接歸零。

階段 2:環境與生命週期防護(Day 16–20)

核心目標:抹平執行環境噪音。排除作業系統版本、彈出式對話框與殘留行程的干擾。

  • 辨識系統 SKU 差異:明確區分 windows-latest(Windows Server,Win32 經典架構)與 windows-11-arm(Windows 11 Client,WinUI 3 現代架構),絕不依賴盲目推論。
  • 引入安靜環境預設:在測試啟動前主動關閉 OOBE 歡迎畫面、Edge 首次啟動提示與 Microsoft Store 幕後自動更新,消除搶奪焦點的系統級彈窗。
  • 實作安全清場機制:按樹狀行程關係保護 Runner;對背景服務型視窗採取隱藏而非強制 taskkill,避免連帶中斷 Console Host。
  • 校準非同步等待時間:冷啟動給予 30 秒餘裕以應對第一次無快取的冷載入;操作後置條件一律採用 1–2 秒高頻輪詢,杜絕固定 sleep。

階段 3:穩健的定位與語意驅動(Day 6–15)

核心目標:跨越渲染與結構異質性。從「看圖找字」轉向真正的「無障礙語意樹」。

  • 揚棄脆弱的視窗標題:改用 Before/After 視窗清冊 Diff、行程執行檔名稱(Process Name)與 Win32 Class Name 鎖定頂層視窗。
  • 建立候選過濾階梯:識別複合控制項的結構邊界,排除如 NotepadTextBox 等無實體內容的外殼容器,精準命中真實承載文字的子節點。
  • 以 Control Pattern 索取能力:永遠不預設控制項型別必定支援特定介面;以動態查詢 GetCurrentPattern 取代靜態型別轉型。
  • 馴服焦點與鍵盤配置:在 Session 層級主動釘住 0409:00000409 英文輸入法;對 XAML 現代應用跨越 Content Island 邊界傳遞焦點。

階段 4:防禦性驗證與持續整合(Day 21–29)

核心目標:杜絕偽陽性與偽陰性。讓綠燈絕對可信,讓紅燈一目了然。

  • 實作 Fail-closed 等待條件:逾時或底層查詢異常時一律拋出明確例外,杜絕因「查不到就回傳空陣列」導致斷言意外通過的偽全綠。
  • 矩陣與應用隔離:每個測試 Job 嚴格限定操作單一被測目標,防止多個桌面程式在同一個 Runner 上互相爭搶前景焦點。
  • 失敗診斷資產化:自動萃取失敗前關鍵 3 秒的影格序列(frames/),並以正則表達式提煉結構化「失敗簽名(Signature)」,加速問題分流。
  • 本機對照實驗室:在開發機搭建與 CI 規格一致的 Windows 11 ARM64 虛擬機,作為疑難雜症在線上 Runner 出現前的本地重現沙盒。

可為與不可為

走完這三十天,除了累積出一套可用的工程架構,同樣重要的,是摸清這條路上哪些事情做得成、哪些事情真的碰不得。受限於 Windows 的安全性架構與渲染本質,以下是現行架構明確做不到的事:

  • 系統安全與渲染限制:無法跨越 Winlogon 桌面操作 UAC 對話框;無法走訪未納入 UIA 視覺樹的 Win32 原生功能表(#32768);無法定位 DirectX 自繪終端機(如 Windows Terminal)的像素級插入游標。
  • 輸入法切換:TSF 現代文字服務不再暴露傳統 IMM32 上下文,切換輸入法必須在 Session 層級釘選純英文鍵盤,無法靠外部 API 偷換。
  • 自動化不等於無障礙:即使能透過 Win32 訊息繞過限制驅動控制項(如 Scintilla),也僅代表「能被測試驅動」,絕不代表「具備無障礙存取能力」。自動化守護的是軟體品質回歸,無障礙缺口只能在控制項本身的 UIA Provider 端補齊。

三十天後的起點

回顧 2020 年代以前,Web 前端測試同樣經歷過極端混亂的蠻荒時期,直到現代瀏覽器標準確立,以及 Playwright、Cypress 等工具鏈將非同步等待、錄影觀測與無頭架構規範化,Web 自動化才真正成為可信任的工程基石。

反觀 Windows 桌面生態,底層 Win32 API 橫跨三十年演進,從 MFC、WinForms、WPF、Qt 到現代的 WinUI 3,技術堆疊極度破碎。許多人因此認為桌面測試註定落後於時代。

但這 30 天向我們揭示了一條完全可行的坦途:

  • GitHub 已經為全球開源專案提供了免費的 x64 與 ARM64 Windows hosted runner。
  • 作業系統底層早就具備了完整的行程追蹤、訊息封送與視窗列舉機制。
  • 我們缺少的從來不是基礎設施,而是一套願意鑽進 Win32 / UIA 泥淖中、將作業系統邊界摸清、並以嚴格後置條件包裝成高階抽象的工具體系。

這 30 天寫下的每一篇文章、每一個坑位分析,全部實作並收斂於開源專案 wintegrate(MIT 授權,pip install wintegrate)。

謝謝你一路讀到第 30 天。
這不是 Windows 桌面自動化的終點,而是它重返現代 CI 世界的起點。
如果你在未來的自動化征途上,踩到了這個系列尚未記錄的全新系統陷阱,歡迎回到 GitHub 開一張 issue。我們永遠在最真實的現場,等待下一個安靜失敗的謎題。


上一篇
Day 29:Windows GUI CI 的防禦性驗證與診斷設計
系列文
Windows 桌面軟體 CI 實戰:從系統工具開發到 Zero-RDP 自動化測試錄影 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言