三十天前,這個系列的第一篇拋出了一個現實問題:為什麼在現代軟體工程中,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 系列);不問「有沒有拋出例外」,而是問「結果是否真正可用」。
一個沒有診斷產物的 GUI 測試,只要在 CI 上失敗一次,整個團隊就會陷入猜謎:是網路斷了?焦點被搶了?對話框跳出來了?還是系統卡死了?
猜謎的成本極高,幾次之後,團隊通常會做出最糟的妥協:「那個測試好像很 flaky,先 @pytest.mark.skip 掉吧。」
測試之所以變成 flaky,是因為你看不見失敗現場。 第一章建立的四個產物:全程 MP4 錄影、視窗普查(Census)、事件時間軸(Timeline)以及失敗瞬間的切格截圖(frames/),並非除錯的輔助裝飾品,它們是整套自動化工程的飛行記錄器。
當 CI 紅掉的那一刻,工程師不需要連進 runner,也不用重推一個 commit 加 log:
READ_THIS_FIRST.md 與例外簽名(Signature),一眼看出是哪個外部行程搶走前景。frames/,直接看見失敗當下的視窗畫面與焦點狀態。session_events.jsonl,精確定位毫秒級的事件順序。診斷產物是阻斷「猜測與重試」循環的唯一武器。 把失敗現場做得越清晰,測試套件的壽命就越長。
我們在開發機上調試時,面對的是一台極度偏袒你的電腦:
但在 GitHub Actions 剛開機的拋棄式 runner 上,這一切假設全部瓦解。 我們實測過:Win11 記事本的冷啟動耗時,可以從本機的幾百毫秒暴增至無人機房內的 7.84 秒,在資源緊繃與換包競爭下甚至飆升至 17.18 秒;Windows Server SKU 缺少 Task View 與現代虛擬桌面 COM 介面;終端機 ConPTY 的 Console Host 沒有傳統視窗,輕率殺行程會直接導致整條 pipeline 自毀。
一個合格的桌面測試工程師,在自己的開發機上寫好每一行程式碼時,都必須反問一句:
「在一台剛開機、沒人在看、字型還沒快取、背景隨時有系統視窗搶焦點的拋棄式 VM 上,這行程式碼還能成立嗎?」
這 30 天裡,作者本人錯得最離譜的幾次判斷,全部來自「聽起來極度合理的邏輯推論」:
import av,直覺推論「PyAV 的 ARM64 wheel 壞了」;實測發現是清場腳本誤殺了共用 Console 的 Windows Terminal,行程死於 0.27 秒內的 KeyboardInterrupt。in_progress 12 分鐘,推論「程式碼發生了 12 分鐘的死結」;實測發現行程早就死透,只是孤兒行程握著 stdio pipe 不放。automation_id 設計都完全不同。自動化除錯中最昂貴的浪費,就是用想像力代替測量。 面對無法解釋的現象:先丟 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 本機對照實驗室 |
核心目標:建立黑箱外的眼睛。在追求任何測試通過率之前,先保證「失敗時看得到現場」。
if: always() 保全上傳 Artifact。現場資料只要丟失一次,該次除錯的時間成本就直接歸零。核心目標:抹平執行環境噪音。排除作業系統版本、彈出式對話框與殘留行程的干擾。
windows-latest(Windows Server,Win32 經典架構)與 windows-11-arm(Windows 11 Client,WinUI 3 現代架構),絕不依賴盲目推論。taskkill,避免連帶中斷 Console Host。sleep。核心目標:跨越渲染與結構異質性。從「看圖找字」轉向真正的「無障礙語意樹」。
NotepadTextBox 等無實體內容的外殼容器,精準命中真實承載文字的子節點。GetCurrentPattern 取代靜態型別轉型。0409:00000409 英文輸入法;對 XAML 現代應用跨越 Content Island 邊界傳遞焦點。核心目標:杜絕偽陽性與偽陰性。讓綠燈絕對可信,讓紅燈一目了然。
frames/),並以正則表達式提煉結構化「失敗簽名(Signature)」,加速問題分流。走完這三十天,除了累積出一套可用的工程架構,同樣重要的,是摸清這條路上哪些事情做得成、哪些事情真的碰不得。受限於 Windows 的安全性架構與渲染本質,以下是現行架構明確做不到的事:
Winlogon 桌面操作 UAC 對話框;無法走訪未納入 UIA 視覺樹的 Win32 原生功能表(#32768);無法定位 DirectX 自繪終端機(如 Windows Terminal)的像素級插入游標。回顧 2020 年代以前,Web 前端測試同樣經歷過極端混亂的蠻荒時期,直到現代瀏覽器標準確立,以及 Playwright、Cypress 等工具鏈將非同步等待、錄影觀測與無頭架構規範化,Web 自動化才真正成為可信任的工程基石。
反觀 Windows 桌面生態,底層 Win32 API 橫跨三十年演進,從 MFC、WinForms、WPF、Qt 到現代的 WinUI 3,技術堆疊極度破碎。許多人因此認為桌面測試註定落後於時代。
但這 30 天向我們揭示了一條完全可行的坦途:
這 30 天寫下的每一篇文章、每一個坑位分析,全部實作並收斂於開源專案 wintegrate(MIT 授權,pip install wintegrate)。
謝謝你一路讀到第 30 天。
這不是 Windows 桌面自動化的終點,而是它重返現代 CI 世界的起點。
如果你在未來的自動化征途上,踩到了這個系列尚未記錄的全新系統陷阱,歡迎回到 GitHub 開一張 issue。我們永遠在最真實的現場,等待下一個安靜失敗的謎題。