【テクニカル・上級編】VB.NETにおけるユニットテストの導入:NUnitとMoqを用いたWindows Formsのロジック層(Presenter/ViewModel)の自動テスト自動化とCI/CDパイプライン連携 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

Windows Formsの呪縛を断つ:NUnitとMoqによるUIロジックの極限テスト自動化とCI/CD実践

レガシーシステムの保全において、最もエンジニアの精神をすり減らすのは「ボタンクリックイベントの裏に1,000行のスパゲッティコードが絡み合い、怖くてリファクタリングできないWindows Formsアプリケーション」の存在だ。

「画面とロジックが密結合しているから、ユニットテストなど不可能だ」
「VB.NETの古い構文とWin32 APIの直接呼び出しが混在しており、モック化など夢のまた夢だ」

もし君が現場でそう諦めているなら、それは建築の基礎を無視して砂上に楼閣を建てているようなものだ。本稿では、UIとビジネスロジックを完全に分離するアーキテクチャ(MVPパターン)の導入から、NUnitとMoqを用いた自動テスト、そしてメモリのライフサイクル管理、果てはCI/CDパイプライン連携に至るまでの全知見を解き明かす。

—

1. なぜWindows Formsのテストは破綻するのか?

多くのVB.NET製レガシーシステムは、以下のようなアンチパターンで構築されている。

.net
‘ 【最悪のアンチパターン】Formクラスに全てが凝縮されたコード
Public Class MainForm
Private Sub btnExecute_Click(sender As Object, e As EventArgs) Handles btnExecute.Click
‘ 1. UIから直接値を取得
Dim rawInput As String = txtInput.Text

‘ 2. Win32 APIを直接叩いてレガシーなメモリ操作やウィンドウ制御
‘ 3. 直接DBに接続してビジネスロジックを実行
Using conn As New SqlConnection(“…”)
‘ … データベース処理 …
End Using

‘ 4. 結果を直接UIに反映
lblResult.Text = “処理完了”
End Sub
End Class

このコードは、自動テストの実行時に`System.Windows.Forms.Form`のインスタンス化を強要し、さらにデータベースやOSのウィンドウハンドル(HWND)といった外部リソースに依存するため、ヘッドレス環境(CIサーバー上など)での実行が不可能になる。

この呪縛を断つ唯一の解が、「Presenter(またはViewModel)」による関心の分離である。

—

2. テスト可能なアーキテクチャへのリファクタリング(MVPパターン)

UI(View)は「愚かな(Dumb)」存在であるべきだ。Viewはユーザー入力を受け取りPresenterに伝え、Presenterからの命令通りに画面を書き換えるだけにする。

ステップ1: Viewの抽象化(インターフェース定義)

まず、フォームが実装すべき契約(Contract)を定義する。これにより、テスト時には本物のWindows Formsの代わりにモックオブジェクトを注入できるようになる。

.net
Namespace Presentation.Views
”’

”’ メイン画面の振る舞いを定義するインターフェース
”’

Public Interface IMainView
Property InputText As String
Property ResultText As String
Sub ShowErrorMessage(message As String)
End Interface
End Namespace

ステップ2: 具象Formでのインターフェース実装

実際のWindows Formsクラスは、このインターフェースを実装するだけの実装素子に落とし込む。

.net
Imports Presentation.Views

Public Class MainForm
Implements IMainView

Private _presenter As MainPresenter

Public Sub New()
InitializeComponent()
‘ 依存性の注入(DI)の簡易的な手動配線
_presenter = New MainPresenter(Me, New BusinessService())
End Sub

‘ インターフェースのプロパティ実装
Public Property InputText As String Implements IMainView.InputText
Get
Return txtInput.Text
End Get
Set(value As String)
txtInput.Text = value
End Set
End Property

Public Property ResultText As String Implements IMainView.ResultText
Get
Return lblResult.Text
End Get
Set(value As String)
lblResult.Text = value
End Set
End Property

Public Sub ShowErrorMessage(message As String) Implements IMainView.ShowErrorMessage
MessageBox.Show(message, “エラー”, MessageBoxButtons.OK, MessageBoxIcon.Error)
End Sub

Private Sub btnExecute_Click(sender As Object, e As EventArgs) Handles btnExecute.Click
‘ ロジックは全てPresenterへ委譲
_presenter.OnExecuteClicked()
End Sub
End Class

ステップ3: ビジネスロジックをカプセル化したPresenter

ここがテストの主戦場となる。外部依存(DBやサービス層)はすべてインターフェース経由でコンストラクタインジェクションする。

.net
Imports Presentation.Views

Public Class MainPresenter
Private ReadOnly _view As IMainView
Private ReadOnly _service As IBusinessService

Public Sub New(view As IMainView, service As IBusinessService)
_view = view
_service = service
End Sub

Public Sub OnExecuteClicked()
Try
Dim input = _view.InputText

‘ バリデーション
If String.IsNullOrWhiteSpace(input) Then
_view.ShowErrorMessage(“入力値が空です。”)
Return
End Sub

‘ ビジネスロジックの実行
Dim result = _service.ProcessData(input)

‘ 結果の反映
_view.ResultText = $”成功: {result}”

Catch ex As Exception
_view.ShowErrorMessage($”予期せぬエラー: {ex.Message}”)
End Try
End Sub
End Class

—

3. NUnitとMoqによるユニットテストの実装

ビジネスロジックとViewが分離されたことで、Windows Formsを一切起動せずに、純粋なメモリ上で高速なユニットテストが実行可能になる。

以下のテストプロジェクト(NUnit + Moq)のコードを見てほしい。

.net
Imports NUnit.Framework
Imports Moq
Imports Presentation.Views


Public Class MainPresenterTests

Private _mockView As Mock(Of IMainView)
Private _mockService As Mock(Of IBusinessService)
Private _presenter As MainPresenter


Public Sub SetUp()
‘ モックの初期化
_mockView = New Mock(Of IMainView)()
_mockService = New Mock(Of IBusinessService)()

‘ テスト対象のインスタンス生成(モックを注入)
_presenter = New MainPresenter(_mockView.Object, _mockService.Object)
End Sub


Public Sub OnExecuteClicked_NormalInput_UpdatesViewWithResult()
‘ Arrange (準備)
_mockView.Setup(Function(v) v.InputText).Returns(“TestInput”)
_mockService.Setup(Function(s) s.ProcessData(“TestInput”)).Returns(“ProcessedValue”)

‘ Act (実行)
_presenter.OnExecuteClicked()

‘ Assert (検証)
‘ ResultText プロパティに正しい値がセットされたかを検証
_mockView.Verify(Sub(v) v.ResultText = “成功: ProcessedValue”, Times.Once)
End Sub


Public Sub OnExecuteClicked_EmptyInput_ShowsErrorMessage()
‘ Arrange
_mockView.Setup(Function(v) v.InputText).Returns(String.Empty)

‘ Act
_presenter.OnExecuteClicked()

‘ Assert
‘ エラーメッセージが表示され、サービス層が呼ばれていないことを検証
_mockView.Verify(Sub(v) v.ShowErrorMessage(It.IsAny(Of String)()), Times.Once)
_mockService.Verify(Function(s) s.ProcessData(It.IsAny(Of String)()), Times.Never)
End Sub

End Class

このテストは、GUIを持たないコンソールやビルドサーバー上で一瞬で(ミリ秒単位で)実行される。Win32 APIのハングアップや、UIスレッドのデッドロックに悩まされることは二度とない。

—

4. レガシー環境・Win32 API連携時のメモリ最適化と注意点

業務システムにおいて、どうしてもレガシーなWin32 API(例: `SendMessage` や `GetWindowText` 等)を叩かざるを得ないシーンがある。VB.NETでこれらを扱う際、ガベージコレクション(GC)の盲点によるメモリリークや、ハンドル枯渇が発生しやすい。

非マネージリソースの明示的解放 (`IDisposable` の徹底)

Win32のハンドルやCOMオブジェクトを扱うクラスは、必ず `IDisposable` パターンを実装し、`Dispose` で明示的に解放しなければならない。

.net
Public Class Win32ApiWrapper
Implements IDisposable

Private _disposed As Boolean = False

‘ 例: アンチエイリアスや特定のハンドル保持を想定
Private _safeHandle As IntPtr

Public Sub New()
‘ 何らかのAPI呼び出しによるハンドル取得
‘ _safeHandle = OpenProcess(…)
End Sub

Protected Overridable Sub Dispose(disposing As Boolean)
If Not _disposed Then
If disposing Then
‘ マネージリソースの解放
End If

‘ アンチエイリアスやWin32ハンドルの解放 (最重要)
If _safeHandle <> IntPtr.Zero Then
‘ CloseHandle(_safeHandle) など
_safeHandle = IntPtr.Zero
End If

_disposed = True
End If
End Sub

‘ ファイテナライザ(万が一の保険)
Protected Overrides Sub Finalize()
Dispose(False)
MyBase.Finalize()
End Sub

Public Sub Dispose() Implements IDisposable.Dispose
Dispose(True)
GC.SuppressFinalize(Me)
End Sub
End Class

UIスレッドとバックグラウンドスレッド間でのCOMオブジェクトのやり取り(STAThreadの維持)や、`Marshal.ReleaseComObject` の適切な適用など、VB.NETの底流にあるCLRとOSの境界を意識したコードが、シニアエンジニアには求められる。

—

5. CI/CDパイプラインとの連携(GitHub Actions / Azure DevOps)

ローカル環境でテストが通るだけでは不十分だ。Gitへのプッシュ、あるいはプルリクエストの作成をトリガーとして、自動的にビルドとユニットテストが走るCI/CDパイプラインを構築する。

以下は、GitHub Actionsを用いたVB.NETプロジェクトの自動テストワークフローの極北だ。

name: VB.NET Windows Forms CI

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

jobs:
build-and-test:
runs-on: windows-latest # Windows FormsのビルドにはWindows環境が必須

steps:

  • name: Checkout Repository

uses: actions/checkout@v4

  • name: Setup .NET Framework (or .NET Core/5/6/7/8)

uses: actions/setup-dotnet@v4
with:
dotnet-version: ‘8.0.x’ # またはレガシーなら MSBuild を使用

  • name: Restore NuGet Packages

run: dotnet restore

  • name: Build Solution

run: dotnet build –configuration Release –no-restore

  • name: Run NUnit Tests

run: dotnet test –configuration Release –no-build –verbosity normal

このパイプラインを導入することで、開発者がレガシーなWindows Formsアプリに対して「リファクタリングの恐怖」を感じることがなくなる。コードを書き換え、プッシュし、CIサーバーが数秒で「テストグリーン」を返却した瞬間、そのコードは保守可能なモダン資産へと昇華しているのだ。

—

結言

Windows Formsは、もはや「古いから捨てられるべき遺物」ではない。適切な設計パターン(MVP / MVVM)と、モックを活用したユニットテストの文化、そしてインフラストラクチャとしてのCI/CDを組み合わせることで、現代の要求水準を満たす頑健なシステム基盤として蘇らせることが可能である。

技術の限界を決めているのはフレームワークではなく、いつだって書いている人間の「意志」なのだから。

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