WPFとVB.NETにおける「品質」の極致:ViewModelの単体テストからFlaUIによるUI自動化、そしてメモリ管理の深淵へ
WPF(Windows Presentation Foundation)を用いたモダンなデスクトップアプリケーション開発において、多くのエンジニアが陥る罠がある。それは、「UIが動くこと」をゴールとし、「品質を検証する仕組み」を後回しにすることだ。
レガシーなVBAやWinFormsからの移行期にあるプロジェクトでは、ロジックがViewに混入し、テスト不可能なコードが蔓延している。しかし、我々のようなアーキテクトが構築すべきは、単なる「動くソフト」ではない。継続的な変更に耐えうる、堅牢な「テスト可能なアーキテクチャ」である。
本稿では、VB.NETを用いたWPF開発において、ViewModelの単体テストから、FlaUIを用いたUI自動テスト、さらにはテスト実行時に発生するメモリリークやゾンビプロセスの制御といった、極限の知見を共有する。
1. MVVMの真髄:テスト可能なViewModelの設計
WPFにおけるテストの第一歩は、View(XAML)からロジックを完全に切り離すことだ。ViewModelは、UIの「状態」と「振る舞い」を司るが、そこには一切の `System.Windows.Controls` への依存があってはならない。
依存性の注入 (Dependency Injection) によるモック化
ビジネスロジックや外部API(DB接続、ファイル操作等)をViewModel内で直接 `New` してはいけない。それはテストにおける死を意味する。インターフェースを介し、テスト時には「Mock(擬似オブジェクト)」を注入できるように設計せよ。
' --- Interface定義: 外部サービスを抽象化 ---
Public Interface IDataService
Function FetchDataAsync(id As String) As Task(Of String)
End Interface
' --- ViewModel: 依存性をConstructorで受け取る ---
Public Class MainViewModel
Implements INotifyPropertyChanged
Private ReadOnly _dataService As IDataService
Private _resultText As String
Public Sub New(dataService As IDataService)
' 依存性の注入。これにより、テスト時にMockを渡せる
_dataService = dataService
End Sub
Public Property ResultText As String
Get
Return _resultText
End Get
Set(value As String)
_resultText = value
OnPropertyChanged(NameOf(ResultText))
End Set
End Property
' 非同期コマンドの実行例
Public Async Function LoadDataCommand(id As String) As Task
Try
' ロジックの実行
ResultText = Await _dataService.FetchDataAsync(id)
Catch ex As Exception
ResultText = "Error: " & ex.Message
End Try
End Function
' INotifyPropertyChangedの実装(省略)
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Protected Sub OnPropertyChanged(propertyName As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
End Sub
End Class
ViewModelの単体テスト (Unit Test)
`Moq` などのライブラリを使用し、ViewModelの挙動を検証する。ここで重要なのは、`Task` の完了を正しく待機し、非同期処理の結果がプロパティに正しく反映されるかを検証することだ。
<TestClass>
Public Class MainViewModelTests
<TestMethod>
Public Async Function Test_LoadDataCommand_Success() As Task
' 1. Arrange: Mockの作成
Dim mockService As New Mock(Of IDataService)()
mockService.Setup(Function(s) s.FetchDataAsync("123")).ReturnsAsync("Success Data")
Dim viewModel As New MainViewModel(mockService.Object)
' 2. Act: コマンド実行
Await viewModel.LoadDataCommand("123")
' 3. Assert: 結果の検証
Assert.AreEqual("Success Data", viewModel.ResultText)
End Function
End Class
2. FlaUIによるUI自動テスト:Viewの挙動を掌握する
ViewModelのテストだけでは不十分だ。XAMLのバインディングが正しいか、ボタンが押せる状態か、といった「実際のUI」の検証が必要となる。ここで、従来のUIAComWrapperやWhiteに代わり、現代的な選択肢となるのが FlaUI である。
AutomationIdの徹底活用
UIテストを安定させる(Flakyなテストを防ぐ)ための鉄則は、`AutomationId` を使用することだ。`Name` や `Content` をターゲットにしてはいけない。それらはローカライズ(多言語化)やデザイン変更によって容易に変化し、テストを破壊するからだ。
XAML側:
<Button x:Name="SubmitButton"
AutomationProperties.AutomationId="BtnSubmit"
Content="送信" />
FlaUIによるテストコード:
Imports FlaUI.Core
Imports FlaUI.UIA3
<TestClass>
Public Class UiAutomationTests
Private _app As Application
Private _window As Window
<TestInitialize>
Public Sub Setup()
' アプリケーションの起動
_app = Application.Launch("YourWpfApp.exe")
Dim automation As New UIA3Automation()
_window = _app.GetMainWindow(automation)
End Sub
<TestMethod>
Public Sub Test_SubmitButton_Click()
' AutomationIdを使用して確実に要素を特定
Dim button = _window.FindFirstDescendant(Function(f) f.AsButton().AutomationId = "BtnSubmit")
Assert.IsNotNull(button, "送信ボタンが見つかりません。")
' クリック操作
button.AsButton().Click()
' 結果の検証(例:ラベルのテキスト変化)
Dim resultLabel = _window.FindFirstDescendant(Function(f) f.AutomationId = "LblResult").AsLabel()
Assert.AreEqual("送信完了", resultLabel.Text)
End Sub
<TestCleanup>
Public Sub Teardown()
' プロセスの確実な終了
_app.Close()
End Sub
End Class
3. アーキテクトの領域:メモリ管理とWindows APIによる制御
大規模な自動テストスイートを運用すると、必ず直面する問題がある。それは「テスト実行後にプロセスが残る(ゾンビプロセス)」ことと、「メモリ消費が指数関数的に増大する」ことだ。
ゾンビプロセスの根絶とWin32 API
FlaUIや`Application.Close()`だけでは、子プロセスやCOMオブジェクトが解放されず、プロセスが残留することがある。これに対し、Windows API (`User32.dll`) を用いて、強制的にウィンドウを特定し、プロセスツリーを掃除する技術が必要だ。
Imports System.Runtime.InteropServices
Public Module ProcessGuardian
<DllImport("user32.dll", SetLastError:=True)>
Private Function PostMessage(hWnd As IntPtr, msg As UInteger, wParam As IntPtr, lParam As IntPtr) As Boolean
End Function
Private Const WM_CLOSE As UInteger = &H10
''' <summary>
''' 指定したプロセスIDのウィンドウに対して、安全かつ強制的にクローズメッセージを送信する
''' </summary>
Public Sub ForceCloseWindow(processId As Integer)
Dim proc = Process.GetProcessById(processId)
For Each win In proc.MainWindowHandle
' 正常なクローズを試みる
PostMessage(win, WM_CLOSE, IntPtr.Zero, IntPtr.Zero)
Next
' 猶予期間の後、強制終了
proc.Kill()
End Sub
End Module
メモリの明示的解放とGCの制御
WPFのテスト、特にUIテストは、大量のVisual TreeとCOMオブジェクトを生成する。これらは通常のガベージコレクション(GC)のサイクルだけでは即座に解放されず、テスト実行中に `OutOfMemoryException` を引き起こす。
テストの `TearDown`(後処理)フェーズにおいて、明示的にオブジェクトのライフサイクルを終了させ、GCを強制駆動させることは、シニアエンジニアの嗜みである。
<TestCleanup>
Public Sub DeepCleanup()
' 1. UI Automation オブジェクトの破棄
If _automation IsNot Nothing Then
_automation.Dispose()
End If
' 2. アプリケーションの終了
_app.Close()
' 3. メモリの強制解放
' UIテストはメモリ負荷が高いため、テストケースごとにGCを回すのが定石
GC.Collect()
GC.WaitForPendingFinalizers()
GC.Collect() ' 二回呼ぶことで、Finalizerによって解放されたオブジェクトを確実に回収する
End Sub
結論:品質は「仕組み」によって担保される
VB.NETを用いたWPF開発において、単なる「実装力」だけでは不十分だ。
1. ViewModelを純粋なロジックとして保ち、DIによってテスト可能にする。
2. FlaUIを活用し、`AutomationId` を起点とした堅牢なUIテストを構築する。
3. Windows APIとGC制御を理解し、テスト環境の安定性を維持する。
これら三位一体の設計こそが、レガシーな環境からモダンなアーキテクチャへと脱皮し、真にプロフェッショナルなソフトウェアを生み出すための唯一の道である。技術の深淵を恐れるな。それを掌握することこそが、我々エンジニアの使命である。
