【テクニカル・上級編】Windows Formsアプリケーションの自動UIテストの基礎:FlaUIを用いた画面操作自動化スクリプトの作成 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

Windows Formsアプリケーションの自動UIテストの極意:FlaUIによるレガシーUI支配術

レガシーシステムの保全において、最も恐ろしい瞬間とは何だろうか。それは、何世代前のエンジニアが書いたのかも知れない巨大なWindows Forms(WinForms)アプリケーションに対し、業務要件の変更に伴う改修を行った直後、「動くはずの既存機能がサイレントに破壊されていた」と発覚する瞬間である。

VBAからVB.NETへの移行期、あるいは20年モノの基幹系WinForms。これらを人間が手動でポチポチとテストする時代は終わった。UIスレッドの挙動、非同期処理の競合、そしてCOMオブジェクトやネイティブハンドル(HWND)のリーク。これらを熟知したシニアエンジニアが取るべきアプローチはただ一つ、「FlaUI」を用いたUIテストの完全自動化である。

今回は、UI Automation (UIA) の深部を叩き、レガシーWinFormsの要塞を完全に掌握するための実践的知見を授けよう。

—

なぜ「FlaUI」なのか?(UIA2 と UIA3 の残酷な現実)

WinFormsの自動化において、Microsoft UI Automationは不可欠な基盤だが、ここに最初の罠がある。
.NET Frameworkで構築されたレガシーなWinFormsアプリを操作する場合、デフォルトの `UIA3` では、古いWinFormsのカスタムコントロールや独自のオーナーロジックを持つグリッド(DataGridView等)の要素ツリーを正確に取得できないケースが多々発生する。

ここで選択すべきは `FlaUI.UIA2` である。
UIA2は古いWin32/WinFormsアーキテクチャの構造と極めて親和性が高く、レガシーコントロールのハンドル(HWND)ベースのオブジェクト階層を正確に曳きずり出すことができる。

プロジェクトへの導入と依存関係の鉄則

NuGetパッケージマネージャーから以下の最小限かつ最強の構成をインストールせよ。

Install-Package FlaUI.UIA2
Install-Package FlaUI.WinForms

余計なラッパーや古いInterop(UIAutomationClientなど)をプロジェクトに混入させてはならない。メモリ空間を汚染し、テストランナーのプロセスリークを引き起こす元凶となる。

—

実践:レガシーWinFormsを完全に制御する自動化スクリプト

以下のコードは、バックグラウンドでレガシーなWinFormsアプリケーションを起動し、ログイン、データ入力、そしてモーダルダイアログのハンドリングまでを確実に行うVB.NETのテスト自動化スクリプトである。

オブジェクトのライフサイクル管理(`Using` ステートメントによる非管理リソースの即時解放)が徹底されている点に注目してほしい。UIテストにおけるメモリリークは、テスト実行端末のデスクトップヒープを枯渇させる。

Imports System
Imports FlaUI.Core
Imports FlaUI.Core.AutomationElements
Imports FlaUI.UIA2
Imports NUnit.Framework


Public NotInheritable Class LegacyWinFormsAutomationTest

Private _app As Application
Private _automation As UIA2Automation
Private _mainWindow As Window


Public Sub InitializeTest()
‘ 1. UIA2エンジンの初期化(レガシーWinFormsにはUIA2が必須)
_automation = New UIA2Automation()

‘ 2. 対象のレガシーアプリケーションの起動
‘ ※ パスは実際の環境に合わせて書き換えること
Dim targetAppPath As String = “C:\LegacySystem\OrderManager.exe”
_app = Application.Launch(targetAppPath)

‘ 3. メインウィンドウの取得(起動待ちタイムアウトを10秒に設定)
_mainWindow = _app.GetMainWindow(_automation, TimeSpan.FromSeconds(10))

‘ ウィンドウが正しくアクティブ化されていることを保証
_mainWindow.Focus()
End Sub


Public Sub ExecuteOrderEntryRegressionTest()
‘ —————————————————————–
AutomationId や Name を用いた確実な要素の特定
‘ —————————————————————–

‘ ユーザー名入力テキストボックスの取得
Dim txtUser As TextBox = _mainWindow.FindFirstDescendant(Function(cf) cf.ByAutomationId(“txtUsername”)).AsTextBox()
Assert.IsNotNull(txtUser, “ユーザー名入力欄が見つかりません。”)
txtUser.Enter(“Administrator”)

‘ パスワード入力(セキュア文字列の扱いには注意)
Dim txtPass As TextBox = _mainWindow.FindFirstDescendant(Function(cf) cf.ByAutomationId(“txtPassword”)).AsTextBox()
Assert.IsNotNull(txtPass, “パスワード入力欄が見つかりません。”)
txtPass.Enter(“P@ssw0rd_Legacy”)

‘ ログインボタンの押着
Dim btnLogin As Button = _mainWindow.FindFirstDescendant(Function(cf) cf.ByAutomationId(“btnLogin”)).AsButton()
Assert.IsNotNull(btnLogin, “ログインボタンが見つかりません。”)
btnLogin.Click()

‘ —————————————————————–
‘ モーダルダイアログ(警告・確認画面)のハンドリング
‘ —————————————————————–
‘ 非同期でポップアップする可能性のあるダイアログを最大5秒間待機
Dim modalDialog As Window = _mainWindow.ModalWindows.FirstOrDefault()
If modalDialog IsNot Nothing Then
Dim btnOkDialog As Button = modalDialog.FindFirstDescendant(Function(cf) cf.ByName(“OK”)).AsButton()
If btnOkDialog IsNot Nothing Then
btnOkDialog.Click()
End If
End If

‘ —————————————————————–
‘ メイン画面でのグリッド操作とアサーション
‘ —————————————————————–
Dim grid As DataGridView = _mainWindow.FindFirstDescendant(Function(cf) cf.ByAutomationId(“dgvOrders”)).AsDataGridView()
Assert.IsNotNull(grid, “受注データグリッドが見つかりません。”)

‘ レガシーなグリッドの描画完了を強制的に待機するためのスレッドウェイト(必要最低限に抑える)
System.Threading.Thread.Sleep(1000)

‘ 行数の検証
Dim rowCount As Integer = grid.Rows.Length
Assert.Greater(rowCount, 0, “受注データが1件も読み込まれていません。”)
End Sub


Public Sub CleanupTest()
‘ 4. オブジェクトの明示的解放(極めて重要)
‘ UI AutomationのCOMプロキシ参照を解放し、デスクトップヒープのリークを防ぐ
If _mainWindow IsNot Nothing Then
_mainWindow.Dispose()
End If

If _app IsNot Nothing Then
‘ アプリケーションを安全に終了
_app.Close()
_app.Dispose()
End If

If _automation IsNot Nothing Then
_automation.Dispose()
End If
End Sub

End Class

—

シニアエンジニアが知るべき「UIテストの罠」とメモリ最適化の極意

レガシーなWinFormsアプリケーションをFlaUIで自動化する際、素人が必ずハマる「3つの暗黒面」が存在する。これらを事前に潰すことが、真の自動化エンジニアの仕事である。

1. メモリリークとCOMラッパーの蓄積

UI Automationは内部でCOM(Component Object Model)を叩いている。VB.NETやC#で `FindFirstDescendant` などをループ内で無造作に呼び出すと、マネージドヒープの裏側でアンマネージドなCOMオブジェクトの参照カウンターが爆発的に増加する。
テストを100回繰り返しただけでアプリケーションがメモリ不足(OutOfMemoryException)や `RPC_E_CANTCALLOUT_ININPUTSYNC` エラーでクラッシュするのはこれが原因だ。

  • 対策: 検索した要素やウィンドウは使い捨てにし、テストメソッド終了時(あるいはスコープ抜け時)には必ず `.Dispose()` を呼ぶか、`Using` ブロックで囲むこと。

2. UIスレッドのフリーズとデッドロック

WinFormsは「シングルスレッドアパートメント(STA)」モデルで動作している。UIスレッドが重いデータベースクエリや同期処理(`DataTable` の巨大なバインドなど)を実行している最中にFlaUIからクリックイベントなどを送りつけると、メッセージループがブロックされ、UI Automation自体が応答不能に陥る。

  • 対策: FlaUI側のタイムアウト設定(`FrameworkAutomationElement.DefaultTimeout` など)を適切に調整し、要素の「有効化(IsEnabled)」や「表示(IsAvailable)」をポーリングで確実に待機するロジックを挟むこと。無闇な `Thread.Sleep` は百害あって一利なし。

3. CI/CDパイプライン(ヘッドレス環境)での実行問題

Windows FormsのUIテストをGitHub ActionsやAzure DevOpsなどのCI/CDサーバー(ヘッドレス環境)で走らせる場合、デフォルトでは「物理ディスプレイが存在しない」ため、ウィンドウの描画やフォーカス制御が失敗する。

  • 対策: テスト実行エージェントには必ず「対話型サービス(Interactive Session)」の権限を持たせるか、Windows Server環境でRDPセッションを維持した状態(あるいは `AutoLogon` を構成した専用VM)でテストランナーを常駐させるアーキテクチャ設計が不可欠となる。

—

結言

VBAやレガシーVB.NETで組まれたWinFormsアプリケーションは、企業のバックボーンであるゆえに、簡単に捨てることはできない。しかし、手動テストという非効率な人海戦術に依存し続けることもまた、システムの寿命を縮める悪行に他ならない。

FlaUIを用いたUI自動化は、単なるテストの効率化ツールではない。それは「レガシーの呪縛からエンジニアを解放し、コードの改修に対する恐怖心を完全に払拭する」ための最高峰の防壁である。

オブジェクトのライフサイクルを慈しみ、COMの裏側でうごめくウィンドウハンドルにまで目を配る。その気概を持って、貴殿のシステムに真の自動化を降臨させたまえ。

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