Windows 桌面應用程式的 UI 自動測試 ── UI Automation 的原理與用 FlaUI 打造不易損壞的測試
· 小村 豪 · 測試, UI Automation, FlaUI, WinForms, WPF, C#, .NET, Windows, CI/CD
「每次發布,都要花整整兩天手動點過所有畫面確認」「上個月修好的地方旁邊,另一個畫面壞了,結果是客戶端先發現的」「網頁團隊已經用 Selenium 或 Playwright 自動化回歸測試了,桌面應用程式卻完全沒動」。這類諮詢,我們常從長期維護 WinForms / WPF 業務應用程式的現場聽到。
另一方面,滿懷幹勁開始寫全畫面測試,卻扛不住維護負擔、一年後放棄的失敗案例,我們也聽過同樣多。UI 自動測試如果在工具選擇和「該做到哪裡」的界線上判斷錯誤,成本會比手動測試還高。反過來,只要理解其原理、設計成不易損壞,並把守備範圍縮小到 smoke test,就能把「發布前的兩天」換成「每晚無人執行的 20 分鐘」,成為一項划算的投資。本文將整理作為基礎的 UI Automation(UIA)原理、工具現況(FlaUI / WinAppDriver / Appium)、以 FlaUI 實作、讓測試不易損壞的設計,一直到 CI 無人執行,把本公司在實際專案中使用的模式完整梳理一遍。
1. 先講結論
- UI 自動測試是測試金字塔的最頂層。它慢、容易損壞、也需要較多時間才能找出失敗原因,因此無法取代單元測試與整合測試。邏輯要靠下層守護,UI 測試則專注在確認「能啟動、主要動線能跑通」的 smoke test(第 6 章)。
- 原理的基礎是 Windows UI Automation(UIA)。從以桌面為根節點的自動化樹(automation tree)中尋找元素,用 AutomationId / Name 等屬性加以定位,再透過控制項模式(Invoke / Value / SelectionItem 等)進行操作。12
- 目標應用程式的元素呈現方式,寫程式碼之前要先用 inspect.exe 或 Accessibility Insights for Windows 確認。inspect 是 Windows SDK 內附的舊版工具,現在官方推薦改用 Accessibility Insights。3
- 本公司推薦的工具是 FlaUI + xUnit / NUnit。FlaUI 是輕薄包裝 UIA 的 MIT 授權 OSS,同時支援 UIA2 / UIA3,現在仍持續積極維護。4
- 曾是 Microsoft 官方選項的 WinAppDriver,最終穩定版停在 2020 年 11 月的 v1.2.1,而伺服器本體是非公開原始碼,社群端也無法自行修復。不建議新導入(第 3 章)。5
- 不易損壞的程度,八成取決於開發端建立 AutomationId 的規範。WPF 是
x:Name或AutomationProperties.AutomationId,WinForms 是Name/AccessibleName。禁止依賴顯示文字(Name)搜尋、座標點擊、Thread.Sleep,改用條件等待(Retry)與 Page Object 模式撰寫(第 4~5 章)。67 - CI 的無人執行必須要有互動式桌面工作階段。以服務方式啟動的代理程式無法運作,畫面鎖定或 RDP 中斷連線也會導致失敗。以自動登入(autologon)方式設定自架 runner,是基本做法(第 7 章)。8
2. 原理 ── UI Automation 的樹狀結構、屬性與模式
2.1 自動化樹(Automation Tree)
UI 自動測試工具並不是把畫面當成圖片來辨識。Windows 有一套官方基礎架構 UI Automation(UIA),讓螢幕報讀器之類的輔助技術能夠以程式方式讀取、操作應用程式的 UI,測試自動化也是搭在這條同一條軌道上運作。1
在 UIA 中,以桌面為根節點,所有開啟中的視窗及其內部的控制項都會以樹狀結構公開出來。按鈕、文字方塊、格線的每一列,全部都是樹上的自動化元素。除了包含所有元素的 raw 檢視外,樹狀結構還有經過篩選的檢視:只留下可操作控制項的控制項檢視(Control View),以及只留下內容的內容檢視(Content View)。1 測試程式碼基本上就是沿著控制項檢視走訪來尋找元素。
這裡要掌握的重點是,只能操作「樹上公開的東西」,而不是「眼睛看到的東西」。用標準控制項組成的畫面會漂亮地出現在樹上,但自行繪製(owner-draw)的清單、用繪圖函式庫畫出的圖面區域、第三方格線的部分內容等,在樹上可能只會被看成「單一個元素」。這種呈現方式,就直接決定了 UI 自動測試能守護的範圍上限。正因如此,寫程式碼之前先確認樹狀結構(2.3 節),才是該做的第一件事。
2.2 屬性與控制項模式
從樹狀結構中定位目標元素的線索是屬性。實務上會用到以下 4 種。
| 屬性 | 內容 | 在測試中的定位 |
|---|---|---|
| AutomationId | 由開發者指定的識別碼,不受語言(locale)影響 | 搜尋鍵的主力。在同層兄弟元素中必須唯一 6 |
| Name | 源自顯示文字的名稱(例如按鈕的標籤) | 人類容易理解,但文案變更、多語系化時容易損壞 |
| ControlType | Button / Edit / ComboBox 等種類 | 用於輔助縮小範圍 |
| ClassName | 實作類別名稱(例如 WinForms 的類別名稱) | 最後手段。實作變更時容易損壞 |
官方定義中,AutomationId「即使 locale 改變也應保持相同值」「在兄弟元素之間應保持唯一」,是專門讓 UI 自動測試能跨語言、跨版本穩定找到元素的屬性。6 反過來說,沒有設定 AutomationId 的應用程式,其 UI 測試就只能依賴顯示文字或樹狀結構位置這類容易損壞的線索。這正是第 5 章要談的主題。
操作找到元素的手段是控制項模式(Control Pattern)。UIA 把「可以點擊」「持有值」「可以選取」這類控制項的功能面,公開成一組獨立於控制項種類之外的模式集合。文件本身也說明「控制項模式與 UI 的關係,就如同 COM 物件與介面的關係」,其設計是向元素詢問「它實作了哪個模式」,再透過該模式進行操作。2 熟悉 COM 的讀者,可以把它想成 UI 版的 QueryInterface。主要的模式如下。
| 模式 | 能做什麼 | 典型控制項 |
|---|---|---|
| Invoke | 執行預設動作(相當於點擊) | 按鈕、選單項目 |
| Value | 取得、設定值 | 文字方塊 |
| SelectionItem / Selection | 選取項目、取得選取狀態 | 清單、下拉選單、分頁 |
| Toggle | 開/關切換 | 核取方塊 |
| ExpandCollapse | 展開/收合 | 下拉選單、樹狀項目 |
| Text | 讀取文字內容 | 文件、RTF 文字 |
| Window | 最大化、最小化、關閉 | 最上層視窗 |
| Scroll / ScrollItem | 捲動、將項目移到可見位置 | 清單、格線 |
「點擊按鈕」的測試程式碼,內部其實是「取得該元素的 Invoke 模式並呼叫 Invoke()」的處理。不是計算座標送出滑鼠事件,而是呼叫控制項自身公開出來的操作──這個差異,正是不依賴視窗位置或 DPI、能保持穩定的測試基礎。
2.3 用 inspect.exe 與 Accessibility Insights 確認「看得到的東西」
目標應用程式的元素究竟以什麼樣的 AutomationId / Name / ControlType / 模式公開出來,最好的方式就是用工具直接看實物。
- inspect.exe:Windows SDK 內附的經典工具(位於 SDK 安裝路徑下的
bin\<version>\<platform>)。用滑鼠或鍵盤焦點選取元素後,會列出 UIA 屬性和模式,也可以驗證樹狀結構的導覽。不過官方將它定位為「舊版工具」,建議改用 Accessibility Insights。3 - Accessibility Insights for Windows:Microsoft 目前推薦的工具。只要滑鼠懸停或移動焦點,就能透過 Live Inspect 方便地確認元素的 UIA 屬性,還附有從無障礙角度出發的自動檢查功能(FastPass)。3
- FlaUInspect:FlaUI 專案附帶的檢視工具。可以分別用 FlaUI 實際使用的 UIA2 / UIA3 視角來確認樹狀結構,如果要用 FlaUI 寫測試,安裝這個工具會很方便。4
依經驗來說,導入 UI 自動測試時最先該做的不是「寫測試程式碼」,而是用 inspect 類工具打開主要畫面,盤點 AutomationId 到底設定了多少。如果這裡空空如也,先從應用程式端的改善(第 5 章)著手,最後反而會更快抵達目標。
3. 工具選項 ── 推薦 FlaUI 的理由與 WinAppDriver 的現況
雖然也可以直接透過 COM 呼叫 UIA,但實務上會使用包裝函式庫。以下依經過查證的事實,整理 2026 年時間點的選項與現況。
| 工具 | 形態 | 現況(2026 年時間點) | 新導入的判斷基準 |
|---|---|---|---|
| FlaUI | .NET 函式庫(MIT) | 積極維護中的 OSS。v5.0.0 於 2025 年 2 月釋出 4 | ◎ 第一候選 |
| WinAppDriver | WebDriver 協定的伺服器(Microsoft) | 最終穩定版 v1.2.1 為 2020 年 11 月。v1.3 仍停留在 2020 年 7 月的 RC 狀態。未解決 issue 超過 1,100 件 5 | △ 實質上已停滯。避免新導入 |
| Appium Windows Driver | Appium 的 Windows 用驅動程式 | 內部使用 WinAppDriver,因此直接繼承上述限制 9 | △ 只在已有 Appium 資產時考慮 |
| Coded UI 測試 | Visual Studio 功能 | VS 2019 起已標示為過時,VS 2026 已移除 10 | × 屬於應遷移的對象 |
3.1 FlaUI ── 作為 UIA 的輕薄包裝,目前最實用
FlaUI 是一套協助 Windows 應用程式(Win32 / WinForms / WPF / 商店應用程式)進行 UI 自動測試的 .NET 函式庫,設計上是作為 Microsoft 原生 UIA 函式庫的包裝層。4 套件由共用部分的 FlaUI.Core,以及依所使用的 UIA 實作而區分的 FlaUI.UIA2 / FlaUI.UIA3 組成。
UIA2 與 UIA3 的使用區分,FlaUI 的 FAQ 有明確說明。UIA2 只有 Managed 實作,不支援觸控等新功能,與 WPF 或商店應用程式的相容性較差;UIA3 是最新實作,最適合 WPF / 商店應用程式,但在 WinForms 應用程式上有時會遇到 UIA2 沒有的缺陷──兩者是這樣的關係。4 換句話說,實務上的判斷基準是:WPF 先試 UIA3,WinForms 先試 UIA2,再依實際應用程式表現,採用較穩定的那一個。兩者都能從 NuGet 安裝並切換嘗試,這正是 FlaUI 作為輕薄包裝層的優點。
由於它本身不含測試框架,因此會搭配 xUnit / NUnit / MSTest,以一般測試專案的方式撰寫。能放進和單元測試相同的測試執行器、相同的 CI 流水線,在維運上是相當大的優勢。
3.2 WinAppDriver ── 老實說,實質上已經停滯
WinAppDriver 是 Microsoft 製作的 UI 測試伺服器,能用和 Selenium 相同的 WebDriver 協定操作 Windows 應用程式,一度被視為主力選項。當 Visual Studio 的 Coded UI 測試被標示為過時(deprecated)時,Microsoft 自己也曾指引遷移方向為「Web 用 Selenium,桌面與 UWP 用 Appium + WinAppDriver」。10
但實際到 GitHub 確認現況,會發現最終穩定版 v1.2.1 是在 2020 年 11 月釋出,之後就沒有再發過穩定版。v1.3 仍停留在 2020 年 7 月的 Release Candidate(v1.2.99)狀態,未成為正式版,未解決的 issue 超過 1,100 件。5 更麻煩的是,GitHub 儲存庫裡只有文件、範例和 issue 追蹤,伺服器本體的原始碼並未公開,也就是說社群根本無法自行修復錯誤。本公司的判斷是:不能因為「官方出品所以放心」,就在 2026 年還把它當作新導入的對象。如果已經有建立在 WinAppDriver 上、正在運作的測試資產,不必馬上捨棄,但建議停止繼續擴充,新的測試流程改用 FlaUI。
3.3 Appium Windows Driver 與其他選項
Appium Windows Driver(appium-windows-driver)是讓 Appium 操作 Windows 應用程式的驅動程式,但它的實體其實是「連到 Microsoft 提供的 WinAppDriver 的介面」,繁重的處理全部由 WinAppDriver 端負責。9 因此它會直接繼承 WinAppDriver 的停滯狀態。對於在行動裝置或網頁上已統一使用 Appium 的團隊、想用 Java 或 Python 撰寫測試程式碼的團隊,它依然是一個選項,但採用前務必先掌握源自 WinAppDriver 的限制與未來走向。
WinUI 3(Windows App SDK)的應用程式也支援 UIA,因此可以用 FlaUI(UIA3)做為測試對象。不過相較於 WinForms / WPF,累積的案例還比較少,各控制項呈現方式的特性,就更需要靠 inspect 類工具事先確認。UI 框架本身的選型,可參考「WinForms/WPF/WinUI 選型指南 - 實務判斷表」的整理。
4. 用 FlaUI 打造最小實作 ── 啟動、搜尋、操作、驗證
理論說到這裡,接下來看實際能跑的程式碼。這是一個透過 NuGet 安裝 FlaUI.UIA3(若在 WinForms 上不穩定則改用 FlaUI.UIA2)與 xUnit 的普通測試專案。
4.1 Smoke Test 的基本形式
「啟動應用程式、開啟訂單登錄對話框、儲存一筆資料、結果顯示在狀態列」這樣的動線測試,可以這樣寫:
using FlaUI.Core;
using FlaUI.Core.AutomationElements;
using FlaUI.Core.Tools;
using FlaUI.UIA3;
using Xunit;
public class OrderSmokeTest
{
[Fact]
public void 建立訂單後主要流程能夠跑通()
{
using var app = Application.Launch(@"C:\App\OrderManager.exe");
using var automation = new UIA3Automation();
try
{
// 等待主視窗出現
var window = app.GetMainWindow(automation);
// 用 AutomationId 尋找元素,並以 Button 進行操作
window.FindFirstDescendant(cf => cf.ByAutomationId("NewOrderButton"))
?.AsButton().Invoke();
// 對話框是非同步開啟的,因此要條件等待「直到它出現」
var dialog = Retry.WhileNull(
() => window.FindFirstDescendant(
cf => cf.ByAutomationId("OrderDialog"))?.AsWindow(),
timeout: TimeSpan.FromSeconds(5)).Result;
Assert.NotNull(dialog);
// 透過 Value 模式輸入(不是模擬鍵盤輸入)
dialog.FindFirstDescendant(cf => cf.ByAutomationId("CustomerNameBox"))
.AsTextBox().Text = "測試商事";
dialog.FindFirstDescendant(cf => cf.ByAutomationId("SaveButton"))
.AsButton().Invoke();
// 儲存完成也透過條件等待狀態顯示來驗證
var saved = Retry.WhileFalse(
() => window.FindFirstDescendant(
cf => cf.ByAutomationId("StatusLabel"))
?.Name.Contains("已儲存") == true,
timeout: TimeSpan.FromSeconds(10));
Assert.True(saved.Success);
}
finally
{
app.Close();
}
}
}
組成要素只有 4 個。
- 啟動:用
Application.Launch啟動處理程序(也可以用Application.Attach連接已經在執行的應用程式)。GetMainWindow會等到能取得主視窗為止。 - 搜尋:
FindFirstDescendant(cf => cf.ByAutomationId(...))就是樹狀搜尋。cf是條件工廠(condition factory),也可以組合ByName/ByControlType/And條件。搜尋鍵如前所述,以 AutomationId 為主力。 - 操作:把找到的元素轉換成
AsButton()/AsTextBox()/AsComboBox()等具型別的包裝物件再操作。Invoke()對應 Invoke 模式,Text屬性對應 Value 模式,2.2 節提到的控制項模式就在背後運作。 - 驗證:對顯示在 UI 上的結果(標籤、清單的行數、視窗標題等)進行 assert。如果要看資料庫的內容,也可以直接在測試程式碼中讀取資料庫。
4.2 用 Retry 等待 ── 寫了 Sleep 就等於認輸
UI 測試不穩定(也就是所謂的 flaky test)的最大來源是時間點(timing)。對話框開啟前、資料載入前、按鈕啟用前──UI 一直都是以非同步方式變化,但如果測試程式碼武斷地認為「應該已經顯示出來了」,就會做出只在慢速機器上失敗的測試。
話雖如此,加入 Thread.Sleep(3000) 是最糟糕的處理方式。在快速機器上會白白等待,在慢速機器上又會因為不夠而失敗,只會不斷累積測試整體的執行時間。答案是條件等待,也就是「持續輪詢直到條件滿足,逾時就中止」,FlaUI 為此提供了專用的 Retry 類別。4
// 等到不是 null(=元素出現)為止,最多等 5 秒
var element = Retry.WhileNull(
() => window.FindFirstDescendant(cf => cf.ByAutomationId("ResultGrid")),
timeout: TimeSpan.FromSeconds(5),
interval: TimeSpan.FromMilliseconds(200),
throwOnTimeout: true).Result;
// 等到條件成立(=按鈕變為可用)為止
Retry.WhileFalse(
() => saveButton.IsEnabled,
timeout: TimeSpan.FromSeconds(5),
throwOnTimeout: true);
系統提供了 Retry.WhileNull / WhileFalse / WhileTrue / WhileException 等方法,可以指定逾時時間、輪詢間隔,以及逾時時是否要拋出例外。4 要注意的是,FlaUI 自 2.0 版起已廢除 Find 系列方法的隱性重試,改為「該在哪裡等待,由測試程式碼明確指定」的方針。4 FindFirstDescendant 只會看到「此刻這一瞬間的樹狀結構」,因此請把「非同步出現的元素搜尋一定要用 Retry 包起來」訂為規範。順帶一提,「不靠 Sleep 硬撐、而是建立等待條件」這種想法本身,不限於 UI 測試,是 Windows 程式設計整體的通用做法(「Windows 中為何應優先使用事件等待而非 Sleep(1)」)。
5. 讓測試不易損壞的設計 ── 應用程式端與測試端的規範
UI 測試因「扛不住維護負擔而被放棄」的原因,幾乎可以歸納成以下 4 點:依賴顯示文字的搜尋、座標點擊、Sleep,以及結構共用不足。我們用規範一一消除這些問題。
5.1 AutomationId 一定要由開發端建立
這是最重要的一點。測試不易損壞的程度,比起測試程式碼怎麼寫,更取決於應用程式是否公開了穩定的識別碼。AutomationId 正是為此而生的屬性,規格要求它不受 locale 影響,且在兄弟元素之間必須唯一。6
在 WPF 中,加上 x:Name 的元素會直接被當作 UIA 端的識別碼使用,因此已命名的控制項不需額外處理就能測試。若要在資料範本(DataTemplate)內等場合明確指定,則設定附加屬性 AutomationProperties.AutomationId。67
<!-- x:Name 直接成為識別碼 -->
<Button x:Name="SaveButton" Content="儲存" Click="OnSave" />
<!-- 範本內等場合明確指定 AutomationProperties.AutomationId -->
<Button AutomationProperties.AutomationId="DeleteRowButton"
Content="刪除"
Command="{Binding DeleteCommand}" />
在 WinForms 中,設計工具裡設定的 Control.Name(把 button1 改成 saveButton 的那個名字)會被用於 UIA 端的識別。依本公司經驗,只要按規範命名的表單,幾乎都能直接用 AutomationId 搜尋,但因框架版本和控制項而呈現方式有所差異,請一定先用 inspect 類工具確認實際的 AutomationId 後,才拿來當測試的搜尋鍵。另外,作為螢幕報讀器讀出名稱的 UIA Name 屬性,在很多控制項上會直接沿用 Text 屬性,但像 TextBox、ListView 這類不會沿用的種類,就必須明確設定 AccessibleName。11 整理 AutomationId 不只是為了測試,同時也完全等同於無障礙功能的整備工作,這一點在公司內部爭取預算時是很好的論點。
規範可以很簡單,本公司會請團隊在程式碼規範裡加入以下兩條。
- 只要控制項有可能成為操作或驗證的對象,就要在畫面上為其加上有意義的
Name(WinForms)/x:Name或AutomationProperties.AutomationId(WPF) - 一旦被測試參照過的識別碼,重新命名時要同步修改測試端(把識別碼當成公開 API 對待)
5.2 禁止座標點擊
「點擊畫面座標 (830, 412)」這類操作,只要視窗位置、解析度、DPI 縮放、主題、字型設定任何一項改變,就會損壞。特別是 DPI,在開發機 100%/CI 機器 150% 這類環境差異下,會大量製造出「本機能過、CI 卻失敗」的情況(DPI 的原理請見「WinForms 的高 DPI 支援」)。如第 2 章所見,透過 UIA 控制項模式進行的操作不依賴座標。FlaUI 雖然也提供直接操作滑鼠的 API,但我們規定只能用在拖放、繪圖畫布這類無法用模式表達的操作。而且即使在這種情況下,也要從元素的 BoundingRectangle 計算相對位置,而不是使用畫面座標。
5.3 用 Page Object 模式把結構集中到一處
如果把 FindFirstDescendant(cf => cf.ByAutomationId("CustomerNameBox")) 這類搜尋程式碼直接寫死在測試本體裡,畫面結構一改變,就得修改所有測試。經典的解法是 Page Object 模式:為「每個畫面(或對話框)建立一個類別」,把元素搜尋與操作全部封閉在裡面。
public sealed class OrderDialogPage
{
private readonly Window _dialog;
public OrderDialogPage(Window dialog) => _dialog = dialog;
// 元素搜尋只寫在這個類別裡
private TextBox CustomerName =>
_dialog.FindFirstDescendant(cf => cf.ByAutomationId("CustomerNameBox")).AsTextBox();
private Button Save =>
_dialog.FindFirstDescendant(cf => cf.ByAutomationId("SaveButton")).AsButton();
private Label Status =>
_dialog.FindFirstDescendant(cf => cf.ByAutomationId("StatusLabel")).AsLabel();
// 測試看到的只有用「業務語言」表達的操作
public void Register(string customerName)
{
CustomerName.Text = customerName;
Save.Invoke();
// 元素搜尋和 null 判定都放在重試裡進行。在儲存後才產生、重新繪製狀態標籤的畫面上,
// 搜尋短暫變成 null 或發生例外屬於正常情況
// (若不加 ignoreException,第一次例外就會直接失敗)
Retry.WhileFalse(() => Status?.Name.Contains("已儲存") == true,
timeout: TimeSpan.FromSeconds(10), throwOnTimeout: true,
ignoreException: true);
}
}
測試本體就能用業務語言寫成 new OrderDialogPage(dialog).Register("測試商事"),畫面變更造成的影響只需修改 Page Object 一處即可。如果應用程式的畫面超過 10 個並要持續維運 UI 測試,這個模式幾乎是必需品。唯一要注意的是,等待條件所參照的元素,請務必像上面的 Status 一樣,維持每次都透過屬性重新搜尋的做法。如果把搜尋結果快取到欄位裡,就會一直抓著因重新繪製而消失的舊元素,白白等到逾時。
5.4 測試的獨立性 ── 不要在測試之間傳遞狀態
每一項 UI 測試都要保持獨立。如果建立「測試 3 是以測試 2 建立的資料為前提」這類順序依賴,一項失敗就會連鎖擴散,也無法調整執行順序。原則如下。
- 每個測試(或測試類別)自行啟動應用程式,結束後務必關閉(用
finally或IDisposablefixture) - 前提資料由測試端自行準備。若應用程式端能提供可切換成測試用設定檔/資料庫的啟動選項,會輕鬆許多
- 別忘了失敗時的收尾工作。沒關乾淨的處理程序或殘留的 modal 對話框,會成為下一個測試失敗的原因(第 7 章)
6. UI 測試該守護到什麼範圍 ── 以 smoke test 為核心劃界
工具備齊之後,總會想測試所有畫面,但這正是分水嶺所在。相比單元測試,UI 測試慢上好幾個量級(單項要數秒到數十秒)、容易損壞(UI 一改就要跟著調整)、找出失敗原因也要花時間(不知道是應用程式的錯、測試的錯,還是環境問題)。測試金字塔最頂層畫得那麼窄,正是因為這種成本結構——能靠下層守護的東西,改用 UI 測試守護永遠是虧本生意。
整理成判斷表如下。
| 想守護的內容 | 適合的層級 | 理由 |
|---|---|---|
| 計算、轉換、業務規則 | 單元測試 | 快速穩定。透過 UI 全面覆蓋根本不划算 |
| 資料庫存取、檔案 I/O、外部整合 | 整合測試 | 使用真實對象,但不需要 UI(如何劃分界線) |
| ViewModel、展示邏輯 | 單元測試 | 若採用 MVVM,可在沒有 UI 的情況下測試 |
| 能啟動、主要動線能跑通、能儲存 | UI smoke test | 這裡才是 UI 測試的主戰場 |
| 過去手動確認時漏掉的致命動線 | UI 測試(回歸) | 只針對造成實際損害的部分追加 |
| 畫面版面、外觀跑掉 | 目視、截圖比對 | 用 assert 寫會陷入維護地獄,數量要收斂 |
| 當機、handle 洩漏等異常狀況 | 另一套基礎架構 | 屬於 UI 測試守備範圍之外(Application Verifier) |
本公司推薦的起步方式是「大約 10 個 smoke test」。「啟動後主畫面出現」「能開啟主要主檔」「能登錄、搜尋、預覽列印一筆代表性單據」「結束時不會出錯」──把發布前一定會手動確認的動線中排名前 10 的直接自動化。這個規模的話,撰寫約需 1~2 週,每晚執行也只要 20~30 分鐘,維護負擔也很現實。等體會到效果之後,再針對曾經造成實際損害的回歸錯誤,一項一項補上防止再發生的測試。反之,以「全畫面、全項目」為目標訂立的計畫,幾乎必定會在途中垮掉。
另一個重要方向,是投資於把邏輯從 UI 抽離出來,而不是增加 UI 測試。業務邏輯直接寫在事件處理程式裡的畫面,只能靠 UI 測試守護;但只要把邏輯搬到 ViewModel 或服務類別,就能改由單元測試守護,UI 測試只需負責「確認接線」即可。以經驗來說,如果覺得需要的 UI 測試數量很多,往往不是測試的問題,而是設計的問題。關於測試整體該如何分層,可參考「如何劃分單元測試與整合測試的界線」;下層測試的實務作法,也請一併參考「自製 Logger 的最低需求與整合測試檢查清單」。
7. CI、無人執行的陷阱 ── 沒有桌面,UI 測試就跑不動
只要 UI 測試都是開發者自己手動執行,一切都很平靜。陷阱集中出現在「CI 每晚無人執行」的階段,這和網頁 UI 測試(用 headless 瀏覽器就能搞定)不同,桌面應用程式的測試需要真正的互動式桌面工作階段,這正是所有問題的根源。
7.1 互動式工作階段是必需品 ── 以服務啟動的代理程式無法運作
CI 代理程式(Azure Pipelines 代理程式、GitHub Actions 的自架 runner 等)通常會以 Windows 服務的形式常駐執行。但服務沒有使用者桌面,因此無法操作從那裡啟動的應用程式視窗。Azure Pipelines 的官方文件也明確指出,執行桌面應用程式 UI 測試的代理程式不能是服務,必須設定為啟用自動登入(autologon)的互動式處理程序。8 此外,Microsoft 主機代理程式(雲端端提供的共用 runner)不支援可見 UI 的測試,只能執行 headless 瀏覽器測試。8 也就是說,桌面應用程式的 UI 測試實質上必須有自架機器(實體機或 VM 皆可)。
autologon 設定有一項官方文件也提及的安全風險:「能實體接觸該機器的人,就能使用自動登入的帳號」。8 前提是要用測試專用帳號、專用機器(VM),並且不要放置生產環境的憑證。
7.2 畫面鎖定、RDP 中斷連線、解析度 ── 常見的失敗模式
即使準備好互動式工作階段,還是有陷阱存在。以下用表格整理症狀與對策。
| 陷阱 | 症狀 | 對策 |
|---|---|---|
| 代理程式以服務方式啟動 | 完全找不到元素、應用程式無法啟動 | 改用互動式處理程序 + autologon 重新設定 8 |
| 畫面鎖定、螢幕保護程式 | 輸入類操作無法傳達,導致失敗 | 在 autologon 設定中停用螢幕保護程式;針對會觸發鎖定的 GPO 申請排除 8 |
| 用「×」中斷 RDP | 中斷的瞬間工作階段會被鎖定,之後的測試全部失敗 | 用 tscon <工作階段 ID> /dest:console 切回主控台後再中斷連線 8 |
| 解析度、DPI 的環境差異 | 本機能通過的測試只在 CI 上失敗 | 固定解析度(Azure Pipelines 有對應的設定工作)。縮放統一為 100% 8 |
| 測試並行執行 | 滑鼠、鍵盤、焦點互相搶奪,導致互相破壞 | UI 測試以一台機器一項、序列方式執行;要平行化就增加機器(VM)數量 |
| 上一次失敗的殘留 | 殘留的處理程序或 modal 對話框,妨礙下一次啟動 | 測試開始前清理目標處理程序;務必用 finally 執行結束處理 |
RDP 的陷阱特別容易踩到,補充說明一下。用遠端桌面登入測試機進行調整,關閉視窗離開後,該工作階段就會變成鎖定狀態,之後的 UI 測試會持續失敗。官方文件建議的迴避方式,是在中斷連線前用系統管理員命令提示字元執行 %windir%\System32\tscon.exe <ID> /dest:console,把工作階段交還給主控台。8 請務必把這一步寫進測試機的維運手冊。
DPI、解析度也需要留意。CI 用的 VM 停留在 1024×768、縮放預設值的情況並不少見,版面配置一改變,就會出現「開發機上能看到的按鈕,不捲動就看不到」之類的差異。只要排除座標點擊,大多能被吸收,但環境還是固定下來比較好。包含應用程式端的 DPI 支援狀況,「WinForms 的高 DPI 支援」中寫的確認角度可以直接套用。
7.3 留下失敗時的證據 ── 截圖與記錄
無人執行的 UI 測試失敗時,如果記錄裡只留下「找不到元素」,還是無法得知原因。要從一開始就內建失敗時儲存截圖的機制。FlaUI 提供擷取畫面或元素的功能,可以從測試框架的失敗掛鉤(hook)中呼叫,並儲存成 CI 的成品(artifact)。
// 從失敗掛鉤等處呼叫。輸出到 CI 的成品儲存位置
FlaUI.Core.Capturing.Capture.Screen()
.ToFile(Path.Combine(artifactDir, $"{testName}_{DateTime.Now:HHmmss}.png"));
除了截圖之外,如果能讓應用程式自己的記錄(處理進行到哪裡)和測試端的記錄(操作成功到哪一步)能用時間戳記互相對照,就能大幅加快判斷「是應用程式的錯、測試的錯,還是環境問題」的速度。若用工作排程器在夜間執行測試時的注意事項(工作階段種類、以 0x1 結束的問題等),請參考「工作排程器的工作沒有執行、以 0x1 結束」一文。
8. 總結
UI 自動測試並不是「裝了工具就會動」,而是要同時具備原理的理解、應用程式端的配合、守備範圍的界線,以及執行環境的設計,才能持續運作下去。以下濃縮要點。
- 基礎是 UI Automation。從樹狀結構用 AutomationId 尋找,透過控制項模式操作。先用 inspect / Accessibility Insights 盤點目標應用程式的呈現方式
- 工具用 FlaUI + xUnit / NUnit。WinAppDriver 自 2020 年以來未再發行穩定版,應避免新導入
- 不易損壞的特性靠規範建立。由開發端建立 AutomationId、禁止座標點擊、禁止 Sleep 並以 Retry 做條件等待、用 Page Object 把結構集中到一處
- 守備範圍從大約 10 個 smoke test 開始。邏輯盡量交給單元、整合測試守護,UI 測試只專注確認「主要動線能跑通」
- CI 的基本形式是自架機器 + 互動式工作階段 + autologon。用維運程序消除畫面鎖定、RDP 中斷連線、解析度差異等陷阱,並務必保留失敗時的截圖
「發布前兩天的手動確認」,只要用範圍收斂得當的 UI 自動測試,就能以現實的成本取代。反過來說,如果現有應用程式完全沒有設定 AutomationId、邏輯又直接寫在畫面事件裡,那麼在寫測試之前,先從應用程式端的小規模改善著手,最終反而是捷徑。無論是判斷該從哪裡下手,還是建立一整套 smoke test,我們都能從應用程式現況調查開始協助。
相關文章
- 如何劃分單元測試與整合測試的界線
- 用 Application Verifier 打造 Windows 異常狀況測試基礎
- 自製 Logger 的最低需求與整合測試檢查清單
- 用 Pester 整理 PowerShell 測試 ── 讓維運腳本不易損壞的實務模式
- WinForms 的高 DPI 支援 ── 4K 螢幕下模糊、跑版的原因與現實對策
相關諮詢領域
合同會社小村軟體(KomuraSoft)承接 WinForms / WPF 應用程式的 UI 自動測試導入(現況調查、AutomationId 整備、建置一整套 smoke test、CI 環境設計),以及既有測試資產遷移至 FlaUI、測試策略整體的設計審查。
參考資料
-
Microsoft Learn, UI Automation Overview. 關於以桌面為根節點的自動化樹、raw / 控制項 / 內容檢視、元素的屬性與控制項模式,以及輔助技術與測試自動化使用同一套基礎架構。 ↩ ↩2 ↩3
-
Microsoft Learn, UI Automation Control Patterns Overview. 關於控制項模式的設計(與 COM 介面的類比)、Invoke / Value / SelectionItem 等模式,以及一個控制項可以實作多個模式。 ↩ ↩2
-
Microsoft Learn, Accessibility tools - Inspect. 關於 inspect.exe 隨 Windows SDK 附帶、可確認 UIA 屬性與模式,以及它被定位為舊版工具、建議改用 Accessibility Insights。 ↩ ↩2 ↩3
-
GitHub, FlaUI/FlaUI. 關於它是支援 Win32 / WinForms / WPF / 商店應用程式的 UIA 包裝層、FlaUI.Core / UIA2 / UIA3 的套件結構、UIA2 與 UIA3 的使用區分(FAQ)、MIT 授權、v5.0.0(2025 年 2 月)的釋出與持續開發,以及 Retry 工具與 2.0 版後廢除 Find 系列方法的隱性重試。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
GitHub, microsoft/WinAppDriver. 關於最終穩定版 v1.2.1 於 2020 年 11 月釋出、v1.3 停留在 2020 年 7 月的 Release Candidate 未再更新、未解決 issue 超過 1,100 件,以及儲存庫以文件、範例為主、伺服器本體原始碼未公開。 ↩ ↩2 ↩3
-
Microsoft Learn, Use the AutomationID Property. 關於 AutomationId 是不受 locale 影響的識別碼、應在兄弟元素間保持唯一、在測試腳本中作為搜尋鍵的使用情境,以及 WPF 中沒有 ID(x:Name)或 x:Uid 的控制項不支援 AutomationId。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, AutomationProperties.AutomationId Attached Property. 關於在 WPF(System.Windows.Automation 命名空間)中,用來設定唯一識別元素字串的附加屬性定義。 ↩ ↩2
-
Microsoft Learn, UI testing considerations (Azure Pipelines). 關於桌面應用程式的 UI 測試需要以啟用 autologon 的互動式處理程序來設定代理程式、Microsoft 主機代理程式不支援可見 UI 測試、RDP 中斷連線造成的鎖定與透過 tscon 的迴避方式、畫面解析度設定工作,以及失敗時的截圖、影片收集。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
GitHub, appium/appium-windows-driver. 關於 Appium Windows Driver 是連到 Microsoft 提供的 WinAppDriver 的介面,以及 README 中提醒 WinAppDriver 伺服器已長期未獲維護。 ↩ ↩2
-
Microsoft Learn, Use Coded UI tests to test your code. 關於 Coded UI 測試已標示為過時、Visual Studio 2019 是最後完整支援的版本,以及當時指引的遷移方向為桌面 / UWP 應用程式改用 Appium + WinAppDriver(VS 2026 中的移除記載於同一份 Learn 文件內的遷移指南)。 ↩ ↩2
-
Microsoft Learn, WinForms: Setting the accessible name on a control. 關於 WinForms 控制項的 UIA Name 屬性在許多種類上會沿用 Text 屬性,以及 TextBox / ListBox 等類型需要明確設定 AccessibleName。 ↩
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
在 WinForms/WPF 應用程式中導入 Entra ID 驗證 ── MSAL.NET 與 WAM 代理的實務架構
從實務角度整理在 WinForms/WPF 桌面應用程式中導入 Entra ID(原 Azure AD)驗證的做法。內容涵蓋公用客戶端的概念、ROPC 廢除現況、應用程式註冊、MSAL.NET 的 AcquireTokenSilent 模式、WAM 代理、權杖快取持久化,以...
委外・委託開發 Windows 應用程式前該整理的事項
在委外・委託開發 Windows 應用程式之前,整理既有軟體改版、設備整合、COM/ActiveX、發布與更新、維護等應留意的重點。
業務應用程式的日期時間與時區 ── 從 DateTime 的陷阱到 UTC 儲存原則、測試設計
伺服器搬遷後時間整整差了 9 個小時、只有海外據點的日期會變成前一天──本文從 DateTime 的 Kind 與隱式轉換整理日期時間事故的根本原因。內容涵蓋與 DateTimeOffset 的取捨、UTC 儲存與 ISO 8601 原則、TimeZoneInfo 與夏令時...
WPF 高 DPI 對應──「不是應該對 DPI 很強嗎」卻仍模糊、暈邊的原因與對策
WPF 以 DIP(1/96 英吋)進行版面配置,一開始就是 System DPI Aware,但移到 DPI 不同的螢幕時整體會暈邊,點陣圖與細線也會模糊。本文從實務角度整理原因判斷、.NET Framework 4.6.2/.NET 上的 Per-Monitor DPI...
WinForms 的高DPI支援 - 4K螢幕顯示模糊、版面跑掉的原因與實務對策
本文從 DPI 虛擬化與 DPI 感知模式(System Aware / Per-Monitor V2)的角度,整理 WinForms 應用程式在4K螢幕、150%縮放環境下顯示模糊、版面跑掉的原因,並說明 .NET 與 .NET Framework 各自的設定方式、Aut...
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
UI 執行緒 & 計時器
整理 WPF / WinForms UI 執行緒、非同步流程、Dispatcher 使用、計時器判斷的主題頁面。
常見問題
整理諮詢這個主題時常見的問題。
- WinForms/WPF 應用程式的 UI 自動測試該用什麼?
- 本公司推薦 FlaUI + xUnit/NUnit。FlaUI 是輕薄包裝 Windows UI Automation(UIA)的 MIT 授權 OSS,同時支援 UIA2/UIA3,2025 年 2 月釋出 v5.0.0,目前仍積極維護中。使用上的判斷基準是:WPF 先試 UIA3,WinForms 先試 UIA2。它本身不含測試框架,因此可以搭配和單元測試一樣的測試執行器、放進同一個 CI 流水線。寫程式碼之前,先用 inspect.exe 或 Accessibility Insights for Windows 確認目標應用程式的元素是如何呈現的,是第一步該做的事。
- 現在還適合新導入 WinAppDriver 嗎?
- 不建議新導入。Microsoft 製作的 WinAppDriver 最終穩定版 v1.2.1 停在 2020 年 11 月,v1.3 仍停留在 2020 年 7 月的 Release Candidate 狀態,未解決的 issue 超過 1,100 件。而且 GitHub 儲存庫裡只有文件和範例,伺服器本體的原始碼並未公開,社群也無法自行修復錯誤。Appium 的 Windows Driver 內部也是使用 WinAppDriver,因此同樣繼承這些限制。如果已經有建立在 WinAppDriver 上的測試資產,不必馬上捨棄,但建議新的測試流程開始改用 FlaUI。
- 要如何防止 UI 自動測試容易損壞?
- 不易損壞的程度,八成取決於開發端建立 AutomationId 的規範。WPF 用 x:Name 或 AutomationProperties.AutomationId,WinForms 用 Control.Name 作為識別碼。在此基礎上,禁止依賴顯示文字(Name)搜尋與座標點擊,改用 FlaUI 的 Retry 類別做條件等待取代 Thread.Sleep,並用 Page Object 模式把每個畫面的元素搜尋與操作都集中到一處。FlaUI 自 2.0 版起已廢除 Find 系列方法的隱性重試,因此請把「非同步出現的元素搜尋一定要用 Retry 包起來」訂為規範。
- UI 自動測試該寫到什麼範圍?
- 建議從大約 10 個 smoke test 開始。UI 測試比單元測試慢上好幾個量級也更容易損壞,因此計算與業務規則交給單元測試、資料庫存取交給整合測試守護,UI 測試只專注確認「能啟動、主要動線能跑通」。把發布前原本要手動確認的動線中排名前 10 的自動化,撰寫大約需要 1~2 週,每晚執行也只需 20~30 分鐘。反過來說,以「全畫面、全項目」為目標的計畫幾乎必定會失敗。如果覺得需要的 UI 測試數量很多,通常是在提示你該把邏輯抽到 ViewModel、改由單元測試守護的設計改善訊號。
作者檔案
本文作者的個人檔案頁面。
Go Komura
小村軟體有限公司 代表
以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。