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

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

脱・スパゲッティコード!WinFormsを「テスト可能なプロの設計」へ進化させる極意

こんにちは。現場の設計と自動化の最前線で、コードの「美しさ」と「堅牢さ」を追求しているチーフアーキテクトです。

あなたがもし、Excelマクロの記録から一歩踏み出し、「もっと大規模で、壊れにくく、メンテナンスしやすいWindowsアプリケーションを作りたい」と考えているなら、今日この場所に来たのは正しい選択です。

これまで、あなたはこう感じたことはありませんか?
「ボタンをクリックした時の処理(`Button1_Click`)の中に、計算も、データベースへの保存も、エラーメッセージの表示も全部書いてしまった……。修正するのが怖くて、触るのが手に余る……」

それは、あなたが悪いのではありません。「UI(見た目)」と「ロジック(中身)」が密結合(べったりとくっついて離れない状態)になっていること、それが原因です。

今日は、Windows Forms(WinForms)の世界で、プロが実践する「テスト可能な設計(MVPパターン)」と、それを支える「NUnit」および「Moq」を使った自動テストの導入方法を、魂を込めて伝授します。

ここをクリアすれば、あなたのVB.NETスキルは「ただ動くコードを書く人」から、「信頼されるシステムを設計するエンジニア」へと昇華しますよ。

1. なぜ、Formの中にコードを書いてはいけないのか?

まず、多くの初心者が陥る「アンチパターン」を見てみましょう。

❌ 避けるべきコード(密結合な設計)

' Form1.vb の中
Private Sub btnCalculate_Click(sender As Object, e As EventArgs) Handles btnCalculate.Click
    ' 1. UIから値を取得
    Dim input As Double = Val(txtInput.Text)

    ' 2. ビジネスロジック(計算)
    Dim result As Double = input * 1.1 ' 消費税計算など

    ' 3. 結果をUIに表示
    lblResult.Text = result.ToString()

    ' 4. データベース保存(もしあれば)
    ' SaveToDatabase(result) 
End Sub

何が問題なのか?
このコードは、`txtInput`(テキストボックス)や`lblResult`(ラベル)といった「画面上の部品」がなければ、絶対にテストができません。
「計算ロジックが正しいか」を確認するためだけに、わざわざアプリを起動し、数字を入力し、ボタンをクリックし……。これでは、プログラムが大きくなった時に、手動テストだけで品質を保つのは不可能です。

2. 解決策:MVP(Model-View-Presenter)パターンの導入

プロの現場では、役割を明確に分ける「関心の分離」を行います。WinFormsにおいて最も相性が良いのがMVPパターンです。

要素 役割 例え話
Model データとビジネスロジック(計算や保存) 厨房の料理人:味を作る専門家
View 画面の表示とユーザー操作の受付 接客係:注文を受け、料理を運ぶ
Presenter ViewとModelを仲介する司令塔 フロアマネージャー:注文を厨房へ伝え、料理を接客係へ渡す

ここで重要な魔法の言葉があります。それは「インターフェース(Interface)」です。
Presenterは「本物のForm」を相手にするのではなく、「こういう機能を持つView(インターフェース)」を相手にするように設計します。これによって、テストの時に「偽物のView(モック)」を差し込めるようになるのです。

3. 実践!テスト可能なアーキテクチャの構築

では、実際に「消費税計算を行うアプリ」を例に、コードを書いていきましょう。

ステップ1:インターフェース(Viewの定義)を作る

まず、Viewが「何ができるか」を定義します。

''' <summary>
''' Viewの振る舞いを定義するインターフェース
''' </summary>
Public Interface IMainView
    Property InputValue As Double
    Property ResultText As String
    Sub ShowErrorMessage(message As String)
End Interface

ステップ2:Model(ロジック)を作る

純粋な計算ロジックだけを記述します。UIのことは一切知りません。

Public Class TaxCalculatorModel
    ''' <summary>
    ''' 消費税を計算する純粋なロジック
    ''' </summary>
    Public Function CalculateTax(amount As Double) As Double
        Return amount * 1.1
    End Function
End Class

ステップ3:Presenter(司令塔)を作る

ここが心臓部です。Presenterは `IMainView` を通じて画面を操作します。

Public Class MainPresenter
    Private ReadOnly _view As IMainView
    Private ReadOnly _model As TaxCalculatorModel

    ' コンストラクタ(依存性の注入:Dependency Injection)
    Public Sub New(view As IMainView, model As TaxCalculatorModel)
        _view = view
        _model = model
    End Sub

    ''' <summary>
    ''' 計算実行ボタンが押された時のロジック
    ''' </summary>
    Public Sub OnCalculateClicked()
        Try
            Dim input = _view.InputValue
            
            ' 入力値チェック
            If input < 0 Then
                _view.ShowErrorMessage("マイナスの値は入力できません。")
                Return
            End If

            ' 計算実行
            Dim result = _model.CalculateTax(input)

            ' 結果をViewに反映
            _view.ResultText = result.ToString("F2")

        Catch ex As Exception
            _view.ShowErrorMessage("予期せぬエラーが発生しました。")
        End Try
    End Sub
End Class

ステップ4:実際のForm(Viewの実装)

Formは、インターフェースの指示通りに「部品」を操作するだけです。

' Form1.vb
Public Class Form1
    Implements IMainView

    Private _presenter As MainPresenter

    Public Sub New()
        InitializeComponent()
        ' 依存関係を組み立てる
        _presenter = New MainPresenter(Me, New TaxCalculatorModel())
    End Sub

    ' UI部品との紐付け
    Public Property InputValue As Double Implements IMainView.InputValue
        Get
            Return Val(txtInput.Text)
        End Get
        Set(value As Double) Implements IMainView.InputValue.SetValue
            txtInput.Text = value.ToString()
        End Set
    End Property

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

    Public Sub ShowErrorMessage(message As String) Implements IMainView.ShowErrorMessage
        MessageBox.Show(message)
    End Sub

    ' ボタンクリック時はPresenterに丸投げ!
    Private Sub btnCalculate_Click(sender As Object, e As EventArgs) Handles btnCalculate.Click
        _presenter.OnCalculateClicked()
    End Sub
End Class

4. NUnit と Moq で「自動テスト」を極める

さて、ここからが本番です。設計が分離できたので、いよいよ「画面を一切出さずに」ロジックをテストできます。

テストプロジェクトを作成し、NuGetで `NUnit` と `Moq` をインストールしてください。

自動テストコードの例

Imports NUnit.Framework
Imports Moq

<TestFixture>
Public Class MainPresenterTests
    Private _mockView As Mock(Of IMainView)
    Private _model As TaxCalculatorModel
    Private _presenter As MainPresenter

    <SetUp>
    Public Sub Setup()
        ' Moqを使って、IMainViewの「偽物(モック)」を作成
        _mockView = New Mock(Of IMainView)()
        _model = New TaxCalculatorModel()
        
        ' Presenterに、本物のModelと「偽物のView」を注入
        _presenter = New MainPresenter(_mockView.Object, _model)
    End Sub

    <Test>
    Public Sub OnCalculateClicked_正当な入力の場合_結果がViewに表示されること()
        ' 【準備】入力値を100に設定
        _mockView.Setup(Function(v) v.InputValue).Returns(100.0)

        ' 【実行】計算処理を走らせる
        _presenter.OnCalculateClicked()

        ' 【検証】ResultTextプロパティに "110.00" がセットされたかを確認
        _mockView.Verify(Function(v) v.ResultText = "110.00", Times.Once())
    End Sub

    <Test>
    Public Sub OnCalculateClicked_マイナスの入力の場合_エラーメッセージが表示されること()
        ' 【準備】入力値を -10 に設定
        _mockView.Setup(Function(v) v.InputValue).Returns(-10.0)

        ' 【実行】
        _presenter.OnCalculateClicked()

        ' 【検証】ShowErrorMessageメソッドが、適切なメッセージで呼ばれたかを確認
        _mockView.Verify(Function(v) v.ShowErrorMessage("マイナスの値は入力できません。"), Times.Once())
    End Sub
End Class

このテストは何をしているのか?

  • Moq (`Mock(Of IMainView)`): `IMainView` のフリをする「身代わり」を作っています。これによって、`MessageBox.Show` が実際にポップアップしてテストが止まることもありません。
  • Setup: 「もしViewの `InputValue` を聞かれたら、100を返してね」という予約をしています。
  • Verify: 「Presenterはちゃんと、Viewに対して『結果を表示しろ』という命令を出したか?」を厳格にチェックしています。

5. CI/CD パイプラインへの連携:品質を自動化する

テストが書けたら、それを「人間が毎回実行する」のはもう卒業しましょう。

現代的な開発では、GitHub Actions や Azure DevOps といったツールを使い、CI/CD(継続的インテグレーション/継続的デリバリー)を構築します。

1. コードをPushする: あなたがコードを書き換えてサーバーへ送る。
2. 自動ビルド: サーバー上で自動的にアプリがビルドされる。
3. 自動テスト実行: 先ほど書いた `NUnit` のテストが、サーバー上で全件実行される。
4. 判定: もし、あなたが計算ロジックを壊してしまったら、テストが失敗し、ビルドがエラーになります。これにより、「壊れたコードが本番環境に流れること」を物理的に防ぐことができます。

最後に:エンジニアとしての第一歩

いかがでしたか?
最初は「インターフェースなんて面倒くさい」「コードの量が増えるじゃないか」と感じるかもしれません。しかし、この「少しの手間」が、数ヶ月後のあなたを救います。

  • 設計を分離する
  • インターフェースを介してやり取りする
  • モックを使ってテストを書く

この3つのステップをマスターすれば、あなたの書くVB.NETは、単なる「動くマクロ」から、プロフェッショナルな「ソフトウェア」へと進化します。

ここをクリアすれば、Visual Basic (VB.NET) の基本、そして設計の本質はバッチリですよ。自信を持って、次のコードを書き進めてください。応援しています!

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