WindowsデスクトップアプリのUI自動テスト ── UI Automationの仕組みとFlaUIで作る壊れにくいテスト

· · テスト, UI Automation, FlaUI, WinForms, WPF, C#, .NET, Windows, CI/CD

「リリースのたびに、全画面を手でクリックして回る確認作業に丸 2 日かかっている」「先月直した箇所の隣で別の画面が壊れていて、客先で見つかった」「Web チームは Selenium や Playwright で回帰テストを自動化しているのに、デスクトップアプリは手つかずのまま」。WinForms / WPF の業務アプリを長く保守している現場から、この種の相談をよく受けます。

一方で、意気込んで全画面のテストを書き始め、メンテナンスに耐えられず 1 年で放棄した、という失敗談も同じくらい聞きます。UI 自動テストは道具の選び方と「どこまでやるか」の線引きを間違えると、手動テストより高くつきます。逆に、仕組みを理解して壊れにくく設計し、守備範囲をスモークテストに絞れば、「リリース前の 2 日」を「毎晩無人で 20 分」に置き換えられる投資になります。この記事では、土台となる UI Automation(UIA)の仕組み、ツールの現状(FlaUI / WinAppDriver / Appium)、FlaUI による実装、壊れにくくする設計、CI での無人実行まで、当社が実案件で使っている型を一通り整理します。

1. まず結論

  • UI 自動テストはテストピラミッドの最上段です。遅く、壊れやすく、失敗原因の特定に時間がかかるため、単体・結合テストの代替にはなりません。ロジックは下の層で守り、UI テストは「起動して主要導線が通る」ことを確認するスモークテストに絞ります(6 章)。
  • 仕組みの土台は Windows UI Automation(UIA)です。デスクトップを根とするオートメーションツリーから要素を探し、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
  • 壊れにくさの 8 割は開発側で AutomationId を振る規約で決まります。WPF は x:NameAutomationProperties.AutomationId、WinForms は Name / AccessibleName です。表示文字列(Name)頼みの検索、座標クリック、Thread.Sleep は禁止し、条件待機(Retry)と Page Object パターンで書きます(4〜5 章)。67
  • CI での無人実行には対話型デスクトップセッションが必須です。サービスとして構成したエージェントでは動かず、画面ロックや RDP 切断でも落ちます。セルフホストのランナーを自動サインイン(autologon)で構成するのが基本形です(7 章)。8

2. 仕組み ── UI Automationのツリー・プロパティ・パターン

2.1 オートメーションツリー

UI 自動テストのツールは、画面を画像として認識しているわけではありません。Windows には、スクリーンリーダーなどの支援技術がアプリの UI をプログラムから読み取り・操作するための公式基盤 UI Automation(UIA) があり、テスト自動化はこの同じレールに乗ります。1

UIA では、デスクトップを根(ルート)として、開いているすべてのウィンドウとその中のコントロールがツリー構造で公開されます。ボタンもテキストボックスもグリッドの行も、すべてツリー上のオートメーション要素です。ツリーには全要素を含む raw ビューのほか、操作対象になるコントロールだけに絞ったコントロールビュー、内容だけのコンテンツビューというフィルター済みのビューがあります。1 テストコードは基本的にコントロールビューを歩いて要素を探すことになります。

ここで押さえておきたいのは、「目に見えているもの」ではなく「ツリーに公開されているもの」しか操作できないことです。標準コントロールで組まれた画面はきれいにツリーへ出ますが、オーナードローで自前描画したリスト、グラフィックライブラリで描いた図面エリア、サードパーティ製のグリッドの一部などは、ツリー上では「ただの 1 要素」にしか見えないことがあります。この見え方が、そのまま UI 自動テストの守備範囲の上限になります。だからこそ、コードを書く前のツリー確認(2.3 節)が最初の作業なのです。

2.2 プロパティとコントロールパターン

ツリーから目的の要素を特定する手がかりがプロパティです。実務で使うのは次の 4 つです。

プロパティ 中身 テストでの位置づけ
AutomationId 開発者が付ける識別子。言語(ロケール)に依存しない 検索キーの本命。兄弟要素の中で一意にする 6
Name 表示文字列由来の名前(ボタンのラベルなど) 人間には分かりやすいが、文言変更・多言語化で壊れる
ControlType Button / Edit / ComboBox などの種別 絞り込みの補助
ClassName 実装クラス名(WinForms のクラス名など) 最後の手段。実装変更で壊れやすい

AutomationId は「ロケールが変わっても同じ値であるべき」「兄弟要素の中で一意であるべき」と公式に定義されており、UI 自動テストが言語やバージョンをまたいで安定して要素を見つけるための専用プロパティです。6 逆に言えば、AutomationId が振られていないアプリの UI テストは、表示文字列やツリー上の位置という壊れやすい手がかりに頼ることになります。ここが 5 章の主題です。

見つけた要素を操作する手段がコントロールパターンです。UIA は「クリックできる」「値を持つ」「選択できる」といったコントロールの機能面を、コントロール種別とは独立したパターンの集合として公開します。ドキュメント自身が「コントロールパターンと UI の関係は、COM オブジェクトとインターフェイスの関係と同じ」と説明しているとおり、要素に「どのパターンを実装しているか」を問い合わせて、そのパターン経由で操作する設計です。2 COM に馴染みのある方には、QueryInterface の UI 版と言えば伝わると思います。主要なパターンは次のとおりです。

パターン できること 典型的なコントロール
Invoke 既定のアクション実行(クリック相当) ボタン、メニュー項目
Value 値の取得・設定 テキストボックス
SelectionItem / Selection 項目の選択・選択状態の取得 リスト、コンボボックス、タブ
Toggle オン・オフの切り替え チェックボックス
ExpandCollapse 展開・折りたたみ コンボボックス、ツリー項目
Text テキスト内容の読み取り ドキュメント、リッチテキスト
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 の現行推奨ツールです。マウスホバーやフォーカス移動だけで要素の UIA プロパティを確認できる Live Inspect が便利で、アクセシビリティ観点の自動チェック(FastPass)も付いています。3
  • FlaUInspect: FlaUI プロジェクト付属のインスペクターです。FlaUI が実際に使う UIA2 / UIA3 それぞれの視点でツリーを確認できるので、FlaUI でテストを書くならこれも入れておくと便利です。4

経験則ですが、UI 自動テストの導入でまず最初にやるべきは「テストコードを書くこと」ではなく、主要画面を inspect 系ツールで開いて、AutomationId がどれだけ振られているかを棚卸しすることです。ここでスカスカだったら、先にアプリ側の改修(5 章)から始めたほうが結局早く着きます。

3. ツールの選択肢 ── FlaUIを推す理由とWinAppDriverの現状

UIA を直接 COM 経由で叩くこともできますが、実務ではラッパーライブラリを使います。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 はマネージド実装のみで、タッチなど新しい機能に対応せず 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 テストが非推奨になった際、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 の停滞をそのまま引き継ぎます。モバイルや Web で 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 スモークテストの基本形

「アプリを起動し、受注登録ダイアログを開き、1 件保存して、結果がステータスに出る」という導線のテストはこう書けます。

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 は条件ファクトリで、ByName / ByControlType / And 条件も組めます。検索キーは前述のとおり AutomationId を本命にします。
  • 操作: 見つけた要素を AsButton() / AsTextBox() / AsComboBox() などの型付きラッパーに変換して操作します。Invoke() は Invoke パターン、Text プロパティは Value パターンと、2.2 節のコントロールパターンがそのまま裏にいます。
  • 検証: UI 上に現れる結果(ラベル、リストの行数、ウィンドウタイトルなど)を assert します。DB の中身まで見たければ、テストコードから直接 DB を読んで構いません。

4.2 待機は Retry で ── Sleep を書いたら負け

UI テストの不安定さ(いわゆる flaky test)の最大の発生源はタイミングです。ダイアログが開くまで、データが読み込まれるまで、ボタンが有効になるまで──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;

// 条件が true になる(=ボタンが有効になる)まで待つ
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 はまさにそのためのプロパティで、ロケールに依存せず、兄弟要素の中で一意であることが求められる仕様です。6

WPF では、x:Name を付けた要素はそれが UIA 側の識別子として使われるため、命名済みのコントロールは追加作業なしでテスト可能です。データテンプレート内などで明示的に付けたい場合は、添付プロパティ AutomationProperties.AutomationId を設定します。67

<!-- x:Name がそのまま識別子になる -->
<Button x:Name="SaveButton" Content="保存" Click="OnSave" />

<!-- テンプレート内などは AutomationProperties.AutomationId を明示 -->
<Button AutomationProperties.AutomationId="DeleteRowButton"
        Content="削除"
        Command="{Binding DeleteCommand}" />

WinForms では、デザイナーで付ける Control.Namebutton1saveButton に変える、あの名前です)が UIA 側の識別に使われます。当社の経験では、Name を規約どおり付けてあるフォームはそのまま AutomationId で検索できることがほとんどですが、フレームワークの世代やコントロールによって見え方に差があるため、必ず inspect 系ツールで実際の AutomationId を確認してからテストの検索キーに使ってください。あわせて、スクリーンリーダー向けの読み上げ名になる UIA の Name プロパティは、多くのコントロールで Text プロパティが流用されますが、TextBox や ListView のように流用されない種別では AccessibleName の明示設定が必要です。11 AutomationId の整備はテストのためだけでなく、アクセシビリティ対応と完全に同じ作業だという点は、社内で予算を通すときの良い材料になります。

規約はシンプルでよく、当社では次の 2 行をコーディング規約に足してもらっています。

  • 画面に置くコントロールには、操作・検証の対象になり得る限り、意味のある 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 パターンで構造を 1 か所に集める

FindFirstDescendant(cf => cf.ByAutomationId("CustomerNameBox")) のような検索コードをテスト本体にベタ書きすると、画面構成が変わったとき全テストを修正する羽目になります。定番の対処が Page Object パターンで、「画面(またはダイアログ)1 つにつきクラス 1 つ」を作り、要素の検索と操作をそこへ閉じ込めます。

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 の修正 1 か所で済みます。画面が 10 を超えるアプリで UI テストを継続運用するなら、このパターンは事実上必須です。1 点だけ注意として、待機条件から参照する要素は上の Status のようにプロパティ経由で毎回検索し直す形を守ってください。検索結果をフィールドにキャッシュすると、再描画で消えた古い要素をつかんだままタイムアウトまで待ち続けます。

5.4 テストの独立性 ── 状態を持ち回らない

UI テストは 1 本 1 本を独立させます。「テスト 3 はテスト 2 が作ったデータ前提」のような順序依存を作ると、1 本の失敗が連鎖し、並べ替えもできなくなります。原則は次のとおりです。

  • 各テスト(またはテストクラス)が自分でアプリを起動し、終わったら確実に閉じる(finallyIDisposable フィクスチャで)
  • 前提データはテスト側が用意する。アプリの設定ファイル・DB をテスト用に差し替えられる起動オプションをアプリ側に用意しておくと一気に楽になります
  • 失敗時の後始末を忘れない。閉じ損ねたプロセスやモーダルダイアログの残骸は、次のテストの失敗原因になります(7 章)

6. どこまでUIテストで守るか ── スモーク中心の線引き

道具が揃うと全画面をテストしたくなりますが、ここが分水嶺です。UI テストは単体テストに比べて桁違いに遅く(1 本数秒〜数十秒)、壊れやすく(UI 変更のたびに追従が要る)、失敗の原因特定に時間がかかります(アプリのバグか、テストのバグか、環境か)。テストピラミッドの最上段が細く描かれるのはこのコスト構造ゆえで、下の層で守れるものを UI テストで守るのは常に損です。

判断表の形にするとこうなります。

守りたいもの 適した層 理由
計算・変換・業務ルール 単体テスト 速く安定。UI 経由で網羅するのは論外
DB アクセス・ファイル I/O・外部連携 結合テスト 本物を使いつつ UI は不要(境界の引き方
ViewModel・プレゼンテーションロジック 単体テスト MVVM なら UI なしでテスト可能
起動できる・主要導線が通る・保存できる UI スモークテスト ここが UI テストの主戦場
過去に手動確認で漏れた致命的導線 UI テスト(回帰) 実害のあった箇所に限定して追加
画面レイアウト・見た目の崩れ 目視・スクリーンショット比較 assert で書くとメンテ地獄。枚数を絞る
クラッシュ・ハンドルリーク等の異常系 別の基盤 UI テストの守備範囲外(Application Verifier

当社がおすすめしている始め方は、「スモーク 10 本前後」です。「起動してメイン画面が出る」「主要マスタが開ける」「代表的な伝票を 1 件登録・検索・印刷プレビューできる」「終了時にエラーが出ない」──リリース前に必ず手で確認していた導線の上位 10 本をそのまま自動化します。この規模なら書くのも 1〜2 週間、毎晩の実行も 20〜30 分に収まり、メンテナンス負荷も現実的です。効果を体感してから、実害のあった回帰バグの再発防止テストを 1 件ずつ足していく。逆に「全画面・全項目」を目標に立てた計画は、ほぼ確実に途中で破綻します。

もうひとつ大事なのは、UI テストを増やす代わりにロジックを UI から引き剥がす方向の投資です。イベントハンドラーに業務ロジックが書き込まれた画面は UI テストでしか守れませんが、ロジックを ViewModel やサービスクラスへ移せば単体テストで守れるようになり、UI テストは「配線の確認」だけで済みます。UI テストの必要本数が多いと感じたら、それはテストの問題ではなく設計の問題であることが多い、というのが実感です。テスト全体をどう層別するかは「ユニットテストと結合テストの境界をどう引くか」、下層テストの実務は「自作ロガーの最小要件と結合テストチェックリスト」もあわせて参照してください。

7. CI・無人実行の罠 ── デスクトップがなければUIテストは動かない

書いた UI テストを開発者の手元で叩いているうちは平和です。罠が集中しているのは「CI で毎晩無人実行する」段階で、Web の UI テスト(ヘッドレスブラウザーで完結)と違い、デスクトップアプリのテストは本物の対話型デスクトップセッションを要求することがすべての根源です。

7.1 対話型セッション必須 ── サービス起動のエージェントでは動かない

CI エージェント(Azure Pipelines エージェント、GitHub Actions のセルフホストランナー等)は、通常 Windows サービスとして常駐させます。しかしサービスにはユーザーのデスクトップがないため、そこから起動したアプリのウィンドウは操作できません。Azure Pipelines の公式ドキュメントも、デスクトップアプリの UI テストを動かすエージェントはサービスではなく、自動サインイン(autologon)を有効にした対話型プロセスとして構成しなければならないと明記しています。8 また、Microsoft ホステッドエージェント(クラウド側で用意される共有ランナー)では可視 UI のテストはサポートされておらず、ヘッドレスブラウザーのテストしか動きません。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 テストは 1 マシン 1 本ずつ直列実行。並列化はマシン(VM)を増やして行う
前回失敗の残骸 残ったプロセスやモーダルダイアログが次回の起動を妨げる テスト開始前に対象プロセスを掃除。終了処理を finally で必ず実行

RDP の罠は特に踏みやすいので補足します。テストマシンにリモートデスクトップで入って調整し、ウィンドウを閉じて退出すると、そのセッションはロックされた状態になり、以後の UI テストは失敗し続けます。公式ドキュメントが案内する回避策は、切断前に管理者コマンドプロンプトで %windir%\System32\tscon.exe <ID> /dest:console を実行してセッションをコンソールへ返すことです。8 テストマシンの運用手順書に必ず入れておいてください。

DPI・解像度も要注意です。CI 用 VM が 1024×768・スケーリング既定のまま、ということは珍しくなく、レイアウトが変わって「開発機では見えていたボタンがスクロールしないと見えない」といった差が出ます。座標クリックを排除していれば大半は吸収できますが、環境は固定するに越したことはありません。アプリ側の DPI 対応状況も含め、「WinFormsの高DPI対応」で書いた確認観点がそのまま使えます。

7.3 失敗時の証拠を残す ── スクリーンショットとログ

無人実行の UI テストが落ちたとき、ログに「要素が見つかりません」とだけ残っていても原因は分かりません。失敗時のスクリーンショット保存を最初から仕込んでおきます。FlaUI には画面や要素をキャプチャする機能があるので、テストフレームワークの失敗フックから呼び出し、CI の成果物(アーティファクト)として保存します。

// 失敗時フックなどから呼ぶ。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 で構造を 1 か所に
  • 守備範囲はスモーク 10 本前後から。ロジックは単体・結合テストへ寄せ、UI テストは「主要導線が通る」ことの確認に徹する
  • CI はセルフホストマシン+対話型セッション+autologonが基本形。画面ロック・RDP 切断・解像度差の罠を運用手順で潰し、失敗時のスクリーンショットを必ず残す

「リリース前の 2 日間の手動確認」は、正しく絞った UI 自動テストなら現実的なコストで置き換えられます。逆に、現状のアプリに AutomationId がまったく振られていない、ロジックが画面イベントに直書きされている、といった場合は、テストを書く前にアプリ側の小さな改修から始めるのが結局の近道です。どこから手を付けるべきかの見立てや、スモークテスト一式の立ち上げは、アプリの現状調査からお手伝いできます。

関連記事

関連する相談領域

合同会社小村ソフトでは、WinForms / WPF アプリへの UI 自動テスト導入(現状調査、AutomationId 整備、スモークテスト一式の構築、CI 環境の設計)や、既存テスト資産の FlaUI への移行、テスト戦略全体の設計レビューを扱っています。

参考リンク

  1. Microsoft Learn, UI Automation Overview. デスクトップを根とするオートメーションツリー、raw / コントロール / コンテンツビュー、要素のプロパティとコントロールパターン、支援技術とテスト自動化が同じ基盤を使うことについて。  2 3

  2. Microsoft Learn, UI Automation Control Patterns Overview. コントロールパターンの設計(COM のインターフェイスとの類比)、Invoke / Value / SelectionItem などのパターンと、1 コントロールが複数パターンを実装できることについて。  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 がロケールに依存しない識別子であること、兄弟要素内で一意であるべきこと、テストスクリプトでの検索キーとしての利用シナリオ、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 へのインターフェイスであり、WinAppDriver サーバーが長期間メンテナンスされていないと README で注意喚起されていることについて。  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 の明示設定が必要なことについて。 

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る