脱・スパゲッティコード!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) の基本、そして設計の本質はバッチリですよ。自信を持って、次のコードを書き進めてください。応援しています!
