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:NameAutomationProperties.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.AutomationId67

<!-- 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 這類不會沿用的種類,就必須明確設定 AccessibleName11 整理 AutomationId 不只是為了測試,同時也完全等同於無障礙功能的整備工作,這一點在公司內部爭取預算時是很好的論點。

規範可以很簡單,本公司會請團隊在程式碼規範裡加入以下兩條。

  • 只要控制項有可能成為操作或驗證的對象,就要在畫面上為其加上有意義的 Name(WinForms)/x:NameAutomationProperties.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 建立的資料為前提」這類順序依賴,一項失敗就會連鎖擴散,也無法調整執行順序。原則如下。

  • 每個測試(或測試類別)自行啟動應用程式,結束後務必關閉(用 finallyIDisposable fixture)
  • 前提資料由測試端自行準備。若應用程式端能提供可切換成測試用設定檔/資料庫的啟動選項,會輕鬆許多
  • 別忘了失敗時的收尾工作。沒關乾淨的處理程序或殘留的 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,我們都能從應用程式現況調查開始協助。

相關文章

相關諮詢領域

合同會社小村軟體(KomuraSoft)承接 WinForms / WPF 應用程式的 UI 自動測試導入(現況調查、AutomationId 整備、建置一整套 smoke test、CI 環境設計),以及既有測試資產遷移至 FlaUI、測試策略整體的設計審查。

參考資料

  1. Microsoft Learn, UI Automation Overview. 關於以桌面為根節點的自動化樹、raw / 控制項 / 內容檢視、元素的屬性與控制項模式,以及輔助技術與測試自動化使用同一套基礎架構。  2 3

  2. Microsoft Learn, UI Automation Control Patterns Overview. 關於控制項模式的設計(與 COM 介面的類比)、Invoke / Value / SelectionItem 等模式,以及一個控制項可以實作多個模式。  2

  3. Microsoft Learn, Accessibility tools - Inspect. 關於 inspect.exe 隨 Windows SDK 附帶、可確認 UIA 屬性與模式,以及它被定位為舊版工具、建議改用 Accessibility Insights。  2 3

  4. 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

  5. GitHub, microsoft/WinAppDriver. 關於最終穩定版 v1.2.1 於 2020 年 11 月釋出、v1.3 停留在 2020 年 7 月的 Release Candidate 未再更新、未解決 issue 超過 1,100 件,以及儲存庫以文件、範例為主、伺服器本體原始碼未公開。  2 3

  6. Microsoft Learn, Use the AutomationID Property. 關於 AutomationId 是不受 locale 影響的識別碼、應在兄弟元素間保持唯一、在測試腳本中作為搜尋鍵的使用情境,以及 WPF 中沒有 ID(x:Name)或 x:Uid 的控制項不支援 AutomationId。  2 3 4 5

  7. Microsoft Learn, AutomationProperties.AutomationId Attached Property. 關於在 WPF(System.Windows.Automation 命名空間)中,用來設定唯一識別元素字串的附加屬性定義。  2

  8. Microsoft Learn, UI testing considerations (Azure Pipelines). 關於桌面應用程式的 UI 測試需要以啟用 autologon 的互動式處理程序來設定代理程式、Microsoft 主機代理程式不支援可見 UI 測試、RDP 中斷連線造成的鎖定與透過 tscon 的迴避方式、畫面解析度設定工作,以及失敗時的截圖、影片收集。  2 3 4 5 6 7 8 9

  9. GitHub, appium/appium-windows-driver. 關於 Appium Windows Driver 是連到 Microsoft 提供的 WinAppDriver 的介面,以及 README 中提醒 WinAppDriver 伺服器已長期未獲維護。  2

  10. Microsoft Learn, Use Coded UI tests to test your code. 關於 Coded UI 測試已標示為過時、Visual Studio 2019 是最後完整支援的版本,以及當時指引的遷移方向為桌面 / UWP 應用程式改用 Appium + WinAppDriver(VS 2026 中的移除記載於同一份 Learn 文件內的遷移指南)。  2

  11. Microsoft Learn, WinForms: Setting the accessible name on a control. 關於 WinForms 控制項的 UIA Name 屬性在許多種類上會沿用 Text 屬性,以及 TextBox / ListBox 等類型需要明確設定 AccessibleName。 

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

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 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽