【実務・中級編】Windows FormsアプリケーションのUI自動化テスト導入:FlaUIを用いた画面遷移と入力値検証のCI/CDパイプライン連携 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

【極限のUI自動化】Windows FormsアプリのテストをFlaUIで完全覚醒させる:画面遷移・入力検証からCI/CD連携まで

日本のエンタープライズITを支え続けるWindows Forms(以下、WinForms)。その堅牢性と生産性の高さから、今なお基幹システムのフロントエンドとして君臨しています。

しかし、現場のリーダーであるあなたに問いたい。「未だに画面のテストを手動で行っていませんか?」

新機能を追加するたびに、Excelの手順書を片手に何十箇所ものテキストボックスに値を打ち込み、グリッドの表示を確認する。そんな不毛な「人間デバッグ」は、今日限りで終わりにしましょう。

今回は、Windows UI自動化フレームワークの事実上の標準(De Facto Standard)である「FlaUI(UIA3)」を採用し、WinFormsアプリケーションの画面遷移、入力値検証、そしてCI/CDパイプラインへの統合までを完全自動化する極限の設計論を伝授します。

単なる「動くコード」ではありません。実務の荒波に耐える「壊れにくく(Robust)」「保守性の高い(Maintainable)」プロダクションコードの書き方を、徹底的に解説します。

—

1. なぜ「FlaUI」なのか?――自動化を挫折させる3大要因の打破

Windowsデスクトップアプリの自動化ツールには、かつてMicrosoft公式の「Coded UI Test(廃止)」や「WinAppDriver(開発停滞)」などがありました。しかし現在、最良の選択肢はFlaUI一択です。

FlaUIは、Windows標準の「UI Automation (UIA) ライブラリ」(UIA2およびUIA3)を洗練されたAPIで包んだオープンソースのラッパーです。特にUIA3は、Windows 10/11の最新のUIフレームワークに最適化されており、圧倒的な動作速度と要素検出の正確性を誇ります。

UI自動化を導入したプロジェクトが陥る、お決まりの挫折パターンを振り返ってみましょう。

1. 「Thread.Sleep」の乱用によるテスト速度の低下と不安定化(Flaky Test)
2. 画面レイアウトの微修正でテストコードが全滅する「密結合設計」
3. ローカル環境では動くが、CI/CDサーバー(ビルドマシン)上で全滅する「Session 0の壁」

本稿では、これらの課題をスマートに解決するアーキテクチャを実装していきます。

—

2. 堅牢なUIテストの設計思想:Page Object Pattern (POP)

Webテスト(Seleniumなど)では定番のPage Object Pattern (Page Objectモデル)を、WinFormsテストにも完全適用します。

テストシナリオの中に「このテキストボックスを探して、この文字を入力して、このボタンをクリックする」という低レイヤーの操作(ハウ・トゥ)を直接書いてはいけません。画面仕様が少し変わっただけで、すべてのテストシナリオが崩壊します。

  • Page Objectクラス: 画面上の要素の探索方法や、画面固有の操作(例:「ログインする」「顧客情報を登録する」)をカプセル化する。
  • テストシナリオクラス: Page Objectが提供する高レベルのAPIを呼び出して、ビジネスロジックの検証(アサーション)に専念する。

[テストシナリオ] (アサーションに専念)
│
▼
[Page Object] (画面のUI要素の探索と操作をカプセル化)
│
▼
[FlaUI / WinFormsアプリ]

—

3. 実践:テスト対象となるWindows Formsアプリ

まずは、テスト対象となる簡単なWinFormsアプリ(VB.NET)を想定します。
顧客コードを入力し、検索ボタンを押すと、データベース(今回はモック)から情報を取得して画面に表示する「顧客検索画面」です。

テスト対象画面(MainForm.vb)のコントロール配置

  • txtCustomerId (TextBox): 顧客ID入力(AutomationId: `txtCustomerId`)
  • btnSearch (Button): 検索実行(AutomationId: `btnSearch`)
  • lblStatus (Label): 結果ステータス(AutomationId: `lblStatus`)
  • lblCustomerName (Label): 顧客名表示(AutomationId: `lblCustomerName`)

> 極意:AutomationIdを必ず設定せよ
> UI自動化において、コントロールの「テキスト名(Textプロパティ)」や「インデックス」で要素を探すのは、テロ行為に等しい設計です。多言語化や文言変更で即座にテストが壊れます。WinFormsでは、コントロールの `Name` プロパティがデフォルトで `AutomationId` としてUIAに公開されます。デザイン画面で命名規則に則った `Name` を必ず設定してください。

—

4. 極限のプロダクションコード:FlaUIによるテスト実装

それでは、テストプロジェクト(VB.NET / .NET 8 or .NET Framework 4.8)を作成し、NuGetから以下のパッケージをインストールします。

  • `FlaUI.UIA3`
  • `MSTest.TestFramework` / `MSTest.TestAdapter`

4.1. Page Objectの実装 (MainPage.vb)

まずは、画面操作をカプセル化するPage Objectを記述します。
ここでのポイントは、「要素が見つかるまで安全に待機する(ポーリング待機)」処理を内包することです。

Imports FlaUI.Core
Imports FlaUI.Core.AutomationElements
Imports FlaUI.Core.Input
Imports FlaUI.Core.WindowsAPI

Public Class MainPage
Private ReadOnly _window As Window

Public Sub New(window As Window)
_window = window ?? Throw New ArgumentNullException(NameOf(window))
End Sub

‘ — UI要素のプロパティ定義 —
‘ AutomationIdをキーにして、型安全に要素を紐付ける

Private ReadOnly Property CustomerIdTextBox As TextBox
Get
Return _window.FindFirstDescendant(Function(cf) cf.ByAutomationId(“txtCustomerId”)).AsTextBox()
End Get
End Property

Private ReadOnly Property SearchButton As Button
Get
Return _window.FindFirstDescendant(Function(cf) cf.ByAutomationId(“btnSearch”)).AsButton()
End Get
End Property

Private ReadOnly Property StatusLabel As Label
Get
Return _window.FindFirstDescendant(Function(cf) cf.ByAutomationId(“lblStatus”)).AsLabel()
End Get
End Property

Private ReadOnly Property CustomerNameLabel As Label
Get
Return _window.FindFirstDescendant(Function(cf) cf.ByAutomationId(“lblCustomerName”)).AsLabel()
End Get
End Property

‘ — 画面操作メソッド(ビジネスアクション) —

”’

”’ 顧客IDを入力して検索を実行する
”’

Public Sub SearchCustomer(customerId As String)
‘ テキスト入力を安全に行う(既存値のクリア含む)
Dim txt = CustomerIdTextBox
txt.Focus()
txt.Text = customerId
Helpers.Wait.UntilInputIsProcessed() ‘ 入力がOSレベルで処理されるのをわずかに待つ

‘ 検索ボタンをクリック
SearchButton.Click()
End Sub

”’

”’ ステータスラベルの文言を取得する(同期制御付き)
”’

Public Function GetStatusText() As String
‘ 非同期処理やDBアクセスのラグを考慮し、空文字でなくなるまで最大5秒待機する(Flaky防止)
Return Retry.WhileEmpty(Function() StatusLabel.Text, TimeSpan.FromSeconds(5)).Value
End Function

”’

”’ 表示されている顧客名を取得する
”’

Public Function GetCustomerNameText() As String
Return CustomerNameLabel.Text
End Function
End Class

4.2. テストシナリオの実装 (CustomerSearchTests.vb)

次に、このPage Objectを利用して、実際のテストシナリオを記述します。
アプリの起動から、正常系・異常系のテスト、そしてクリーンアップ処理までを厳密に管理します。

Imports System.IO
Imports FlaUI.Core
Imports FlaUI.UIA3
Imports Microsoft.VisualStudio.TestTools.UnitTesting


Public Class CustomerSearchTests
Private _automation As UIA3Automation
Private _app As Application
Private _mainPage As MainPage

‘ テスト対象アプリの実行ファイルパス(ビルド構成に合わせて変更)
Private Const TargetAppPath As String = “..\..\..\TargetWinFormsApp\bin\Debug\net8.0-windows\TargetWinFormsApp.exe”

”’

”’ 各テストメソッドの実行前に、アプリをクリーンな状態で起動する
”’


Public Sub SetUp()
‘ UIA3ドライバーの初期化
_automation = New UIA3Automation()

‘ プロセスが既に残っている場合は強制終了させておく(テストの冪等性を担保)
Dim fullPath = Path.GetFullPath(TargetAppPath)
If Not File.Exists(fullPath) Then
Throw New FileNotFoundException($”テスト対象アプリが見つかりません: {fullPath}”)
End If

‘ アプリケーションの起動
_app = Application.Launch(fullPath)

‘ メインウィンドウの取得(表示されるまで最大10秒待機)
Dim mainWindow = _app.GetMainWindow(_automation, TimeSpan.FromSeconds(10))
Assert.IsNotNull(mainWindow, “メインウィンドウの起動に失敗しました。”)

‘ Page Objectのインスタンス化
_mainPage = New MainPage(mainWindow)
End Sub

”’

”’ 各テストメソッドの実行後に、アプリを確実に終了しリソースを解放する
”’


Public Sub TearDown()
‘ 確実にアプリを閉じ、COMオブジェクトを解放する
If _app IsNot Nothing Then
_app.Close()
_app.Dispose()
End If
If _automation IsNot Nothing Then
_automation.Dispose()
End If
End Sub

‘ — テストケース —


Public Sub Test_正常系_存在する顧客IDを入力した場合_顧客名が表示されること()
‘ Arrange
Dim targetId = “CUST-001”
Dim expectedName = “株式会社 帝国重工”
Dim expectedStatus = “検索成功”

‘ Act
_mainPage.SearchCustomer(targetId)

‘ Assert
‘ DBアクセス等の遅延があってもリトライ機構により安全に値が取得できる
Assert.AreEqual(expectedStatus, _mainPage.GetStatusText())
Assert.AreEqual(expectedName, _mainPage.GetCustomerNameText())
End Sub


Public Sub Test_異常系_存在しない顧客IDを入力した場合_エラーメッセージが表示されること()
‘ Arrange
Dim targetId = “CUST-999”
Dim expectedStatus = “対象の顧客は見つかりません”

‘ Act
_mainPage.SearchCustomer(targetId)

‘ Assert
Assert.AreEqual(expectedStatus, _mainPage.GetStatusText())
Assert.AreEqual(String.Empty, _mainPage.GetCustomerNameText())
End Sub
End Class

—

5. データベース・ファイル連携テストにおける「ステート・アイソレーション(状態分離)」

UIテストを実行する際、データベースのデータ状態に依存したテストを書くと、並行実行や複数回実行した際に高確率でテストが落ちます。これを防ぐための鉄則が「ステート・アイソレーション」です。

5.1. 接続先を「テスト用サンドボックス」に差し替える

本番や開発用のデータベースを直接参照してはいけません。
テスト実行時に、テスト対象アプリの `App.config` または `appsettings.json` を動的に書き換える、あるいは起動引数(コマンドライン引数)でテスト用のLocalDBやSQLite等の接続文字列を渡せるように設計してください。

‘ 例:テスト用データベースを初期化するコマンドライン引数を渡してアプリを起動する
_app = Application.Launch(TargetAppPath, “–use-test-db”)

5.2. テストのセットアップでデータを「初期化」する

テストメソッド実行前に、データベースを既知の初期状態にリセットします。
Dapperなどの軽量ORMや、SQLスクリプトの実行を `` 内で行うのが最も確実です。


Public Sub SetUp()
‘ 1. データベースを「CUST-001」が存在する状態にリセットするSQLを流す
DatabaseFixture.ResetToDefaultState()

‘ 2. アプリケーション起動…
End Sub

—

6. CI/CDパイプライン(GitHub Actions)への極限統合

UI自動化テストの真価は、コミットのたびに自動実行されるCI/CD環境で発揮されます。しかし、WindowsのGUIテストには「セッション0の壁」が立ちはだかります。

Windowsサービスとして実行されるCIエージェント(Session 0)は、デスクトップ画面(GUIセッション)を持たないため、WinFormsアプリを起動しようとした瞬間にエラーで即死します。

これを解決するには、「インタラクティブモード(GUIが描画できる状態)でWindowsランナーを動かす」必要があります。

GitHub Actions用のワークフロー定義(.github/workflows/ui-tests.yml)

GitHubホストランナー(`windows-latest`)は、デフォルトでGUIセッションが有効化されているため、特段のハックをせずともFlaUIのテストを実行可能です。ただし、画面解像度を適切に設定しないと、要素の座標ズレや描画遅延でテストが失敗することがあります。

以下に、実戦投入可能なGitHub Actionsの定義ファイルを示します。

name: WinForms UI Automation Tests

on:
push:
branches: [ “main”, “develop” ]
pull_request:
branches: [ “main” ]

jobs:
test:
runs-on: windows-latest

steps:

  • name: Checkout code

uses: actions/checkout@v4

  • name: Setup MSBuild

uses: microsoft/setup-msbuild@v2

  • name: Setup .NET

uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.x

  • name: Restore NuGet packages

run: nuget restore MySolution.sln

  • name: Build Application and Test Project

run: msbuild MySolution.sln /p:Configuration=Release /p:Platform=”Any CPU”

# GUIテストを安定させるため、仮想ディスプレイの解像度を明示的に設定

  • name: Set Screen Resolution

run: |
Set-DisplayResolution -Width 1920 -Height 1080 -Force

# VSTest.Console または dotnet test でテストを実行

  • name: Run UI Tests

run: |
dotnet test MySolution.Tests/MySolution.Tests.vbproj –configuration Release –logger “trx;LogFileName=test_results.trx”

# テストが失敗した場合、証跡としてスクリーンショットを保存する仕組みをテストに仕込んでおき、それを成果物としてアップロードする

  • name: Upload Test Results

uses: actions/upload-artifact@v4
if: always()
with:
name: ui-test-results
path: |
/TestResults/.trx
/TestResults/.png

> プロの知恵:失敗時のスクリーンショットを保存せよ
> テストが失敗した原因(ダイアログが出て止まっている、要素が重なっている等)をCI上で特定するのは困難を極めます。FlaUIの `Capture.Screen()` メソッドを使用し、テスト失敗の例外を検知した際に自動的にスクリーンショットを保存するロジックを `TearDown` に仕込んでおくべきです。
>
> If TestContext.CurrentTestOutcome <> UnitTestOutcome.Passed Then
> Dim image = FlaUI.Core.Capturing.Capture.Screen()
> image.ToFile(Path.Combine(TargetOutputPath, $”{TestContext.TestName}_failed.png”))
> End If
>

—

7. まとめ:手動テストの呪縛から脱却し、攻めのリファクタリングへ

WinFormsアプリケーションの保守で最も恐ろしいのは、「画面の一部を修正したことで、無関係な別の画面が壊れる(デグレード)」ことです。そして、それを恐れるあまり、コードがスパゲティ化していると知りつつもリファクタリングに手を付けられなくなる「技術的負債の悪循環」に陥ります。

FlaUIを用いた強固なUIテストスイートは、この悪循環を断ち切る最強の「安全ネット」です。

1. AutomationId をコントロールに付与し、UI構造とテストを疎結合にする。
2. Page Object Pattern で、テストのメンテナンスコストを劇的に下げる。
3. ポーリングによる動的待機を徹底し、Flakyなテスト(不安定なテスト)を根絶する。
4. CI/CD環境での実行を視野に入れ、ステートのクリーンアップと解像度を制御する。

この4つの原則を愚直に守り、自動テストをパイプラインに組み込むことで、あなたのチームは「壊れる恐怖」から完全に解放されます。今日から手動テストの手順書を捨て、コードで品質を担保するモダンなWinForms開発への一歩を踏み出しましょう。

タイトルとURLをコピーしました