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

スポンサーリンク
スポンサーリンク

運命の分離:Windows Formsにおけるロジックの解体と、NUnit/Moqによる堅牢なる自動テストへの昇華

Windows Forms(以下、WinForms)という領域は、多くのエンジニアにとって「聖域」であり、同時に「泥沼」でもある。

長年、VBAからVB.NETへと継承されてきたレガシーな資産、そして現場で求められる「動くこと」への圧力。その果てに我々が手にするのは、`Button_Click`イベントの中にビジネスロジック、DBアクセス、さらにはWindows APIの呼び出しが渾然一体となった、いわゆる「神オブジェクト(God Object)」としてのFormクラスである。

このような設計は、一見すると開発スピードが速いように見える。しかし、それは技術的負債という名の高利貸しから金を借りているに過ぎない。変更に対する脆弱性、回帰テストの欠如、そして何より「テスト不能」という絶望。これらは、システムが肥大化するにつれ、致命的な破綻を招く。

本稿では、WinFormsのUI層からロジックを剥離させ、NUnitとMoqを用いて「テスト可能なアーキテクチャ」へと再構築する、極限の設計手法を伝授する。

1. アーキテクチャの転換:MVPパターンの採用

WinFormsにおいて、UI(Form)とロジックを分離するための最も現実的かつ強力な解は、MVP (Model-View-Presenter) パターンである。

従来の「Formが全てを行う」設計を捨て、以下の役割分担を徹底する。

  • View (Form): ユーザーの入力を受け取り、表示を更新する「器」に徹する。ロジックは一切持たない。
  • Presenter: Viewからの入力を解釈し、Modelを操作し、その結果をViewに指示する「司令塔」である。
  • Model (Service/Repository): ビジネスルール、データアクセス、外部API呼び出しを担う「実体」である。

ここで肝となるのは、PresenterがForm(具体的なクラス)に依存せず、Viewの「インターフェース」にのみ依存することである。これが、テストにおける「モック(Mock)」を可能にする鍵となる。

2. 実装の極意:インターフェースによる疎結合化

まず、Viewが提供すべき振る舞いをインターフェースとして定義する。

''' <summary>
''' メイン画面の振る舞いを定義するインターフェース。
''' Presenterはこのインターフェースを通じてのみViewを操作する。
''' </summary>
Public Interface IMainView
    Property CustomerName As String
    Property StatusMessage As String
    
    ' Viewで発生したイベントをPresenterへ通知するためのイベント
    Event SaveRequested As EventHandler
    Event CloseRequested As EventHandler
End Interface

次に、このインターフェースを実装するFormと、ロジックを司るPresenterを実装する。

''' <summary>
''' ビジネスロジックを制御するPresenter。
''' </summary>
Public Class MainPresenter
    Implements IMainView ' 実際にはPresenterはViewを制御する側

    Private ReadOnly _view As IMainView
    Private ReadOnly _customerService As ICustomerService ' 外部連携用のサービス

    Public Sub New(view As IMainView, service As ICustomerService)
        _view = view
        _customerService = service

        ' Viewのイベントを購読し、Presenterのメソッドへ橋渡しする
        AddHandler _view.SaveRequested, AddressOf OnSaveRequested
    End Sub

    ''' <summary>
    ''' 保存処理のロジック。UIの存在を知らず、インターフェースのみを操作する。
    ''' </summary>
    Private Sub OnSaveRequested(sender As Object, e As EventArgs)
        Try
            Dim name = _view.CustomerName
            If String.IsNullOrWhiteSpace(name) Then
                _view.StatusMessage = "エラー: 名前を入力してください。"
                Return
            End If

            ' サービス層を呼び出し
            _customerService.SaveCustomer(name)
            _view.StatusMessage = "保存に成功しました。"
        Catch ex As Exception
            ' ログ出力やエラーハンドリングをここに集約
            _view.StatusMessage = $"致命的なエラー: {ex.Message}"
        End Try
    End Sub
End Class

3. NUnitとMoqによる「死の淵」からの脱出:ユニットテスト

Presenterがインターフェースに依存しているため、我々は「本物のForm」を起動することなく、Presenterのロジックを検証できる。これが自動テストの真髄である。

Imports NUnit.Framework
Imports Moq

<TestFixture>
Public Class MainPresenterTests

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

    <SetUp>
    Public Sub Setup()
        ' 各テストの前にモックを初期化
        _mockView = New Mock(Of IMainView)()
        _mockService = New Mock(Of ICustomerService)()
        
        ' Presenterの生成(依存性の注入: DI)
        _presenter = New MainPresenter(_mockView.Object, _mockService.Object)
    End Sub

    <Test>
    Public Sub SaveRequested_WhenNameIsEmpty_ShouldShowErrorMessage()
        ' Arrange: 名前が空の状態を設定
        _mockView.Setup(Function(v) v.CustomerName).Returns(String.Empty)

        ' Act: ViewからのSaveRequestedイベントを強制的に発生させる
        ' (Presenter内のAddHandlerによって、内部のOnSaveRequestedが呼ばれる)
        _mockView.RaiseEvent(Function(v) v.SaveRequested(Nothing, EventArgs.Empty))

        ' Assert: StatusMessageがエラーメッセージに更新されているか
        _mockView.Verify(Function(v) v.StatusMessage = "エラー: 名前を入力してください。", Times.Once)
        
        ' Assert: サービス層の保存メソッドが呼ばれていないことを確認
        _mockService.Verify(Function(s) s.SaveCustomer(It.IsAny(Of String)()), Times.Never)
    End Sub

    <Test>
    Public Sub SaveRequested_WhenNameIsValid_ShouldCallServiceAndShowSuccess()
        ' Arrange: 有効な名前を設定
        _mockView.Setup(Function(v) v.CustomerName).Returns("Legendary Architect")

        ' Act
        _mockView.RaiseEvent(Function(v) v.SaveRequested(Nothing, EventArgs.Empty))

        ' Assert: サービス層のSaveCustomerが正しい引数で呼ばれたか
        _mockService.Verify(Function(s) s.SaveCustomer("Legendary Architect"), Times.Once)
        
        ' Assert: 成功メッセージが表示されたか
        _mockView.Verify(Function(v) v.StatusMessage = "保存に成功しました。", Times.Once)
    End Sub
End Class

4. 伝説のアーキテクトが語る、メモリとリソースの極限管理

WinFormsの設計において、テストコードの品質と同等に重要なのが、オブジェクトのライフサイクル管理である。

イベントハンドラの「メモリリーク」という罠

PresenterがViewのイベントを `AddHandler` で購読する場合、Presenterが破棄されてもView(Form)が生き続けていると、PresenterがGC(ガベージコレクション)の対象にならず、メモリリークを引き起こす。

大規模な業務アプリケーションでは、Presenterに `IDisposable` を実装させ、明示的に `RemoveHandler` を行う責務を課すべきである。

Public Class MainPresenter
    Implements IDisposable

    ' ... 前述のコード ...

    Public Sub Dispose() Implements IDisposable.Dispose
        ' イベントの解除を怠ってはならない。これがメモリ管理の鉄則である。
        RemoveHandler _view.SaveRequested, AddressOf OnSaveRequested
        ' 必要に応じて他のリソースも解放する
    End Sub
End Class

Windows API呼び出しの隔離

もしロジック内で `user32.dll` などのWindows APIを呼び出す必要がある場合、それも直接Presenterに書くべきではない。必ず `IWindowsApiWrapper` のようなインターフェースに隠蔽し、テスト時にはモックに差し替えよ。APIの呼び出しを直接テストしようとする試みは、非決定的なテストを生み、CI/CDの信頼性を破壊する。

5. CI/CDパイプラインへの統合:継続的な品質の担保

設計が整ったら、これらを自動化のサイクルに組み込む。

1. Build Stage: `MSBuild` を使用して、ソリューション全体をビルドする。
2. Test Stage: `dotnet test` または `NUnit Console Runner` を実行し、作成したユニットテストを回す。
3. Artifact Stage: テスト結果(XML形式)を収集し、コードカバレッジ(Coverlet等を使用)を確認する。

GitHub ActionsやAzure DevOps Pipelinesを用いれば、プルリクエストが作成されるたびに、この「防衛線」が自動的に機能する。これにより、レガシーなコードベースであっても、新たな破壊的変更を最小限に食い止めることが可能となる。

結語

Windows Formsは、決して古い技術ではない。それは、現在進行形で動いている巨大な経済基盤の一部である。その基盤を、個人の「勘」や「経験」という不安定なものに頼るのではなく、数学的な裏付けを持つ「テスト」と、堅牢な「アーキテクチャ」によって支えること。

これこそが、真のシニアエンジニアが果たすべき責務である。

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