Windows Formsを脱・レガシー化せよ:NUnitとMoqで実現するVB.NETロジック層の完全テスト自動化とCI/CD連携
Windows Forms(WinForms)アプリケーションの開発において、こんな悪夢のような経験はないだろうか。
「テキストボックスの値を取って、DBに突いて、画面を更新する」という数行のコードを書き換えただけで、なぜか全く関係ない画面のバリデーションが崩壊した。
「動かすには実際に画面を立ち上げてポチポチ手動テストするしかない」ため、リリース前の回帰テストに数日を費やしている。
VB.NETとWindows Formsの組み合わせは、業務アプリケーションの迅速な立ち上げにおいて今なお強力だが、「UIとビジネスロジックがコードビハインド(`Form.vb`)の中でヘドロのように絡み合っている」という致命的な構造的欠陥を抱えがちだ。
今回は、このレガシーな泥沼から抜け出し、NUnitとMoqを用いてビジネスロジックを完全に分離・自動テスト化し、さらにCI/CDパイプラインへと組み込む極限のアーキテクチャ設計を伝授する。妥協のないプロフェッショナルのコードを見ていこう。
—
1. なぜ「動くスパゲッティコード」は破綻するのか?
多くの開発者は、フォーム上のボタンクリックイベントに直接SQLを発行し、結果をグリッドにバインドする。
‘ 【アンチパターン】これぞ保守性ゼロのレガシーコード
Private Sub btnSubmit_Click(sender As Object, e As EventArgs) Handles btnSubmit.Click
If txtUserName.Text = “” Then
MessageBox.Show(“名前を入力してください”)
Return
End If
‘ DB接続とビジネスロジックがUIスレッドに直書きされている
Using conn As New SqlConnection(“…”)
‘ … 直接クエリ実行 …
End Using
lblStatus.Text = “保存完了”
End Sub
このコードの何が問題か?
1. テスト不可能性: このコードをテストするには、実際にアプリケーションを起動し、UI要素をマウスで操作し、背後に本物のデータベースを用意しなければならない。自動テストなど夢のまた夢だ。
2. 変更耐性の欠如: DBの仕様変更やバリデーションルールの追加があった際、UIのロジックごと破壊するリスクが常につきまとう。
これを解決唯一の解法が、MVP(Model-View-Presenter)パターン または MVVMパターン による関心事の分離である。今回は、WinFormsと極めて相性が良いMVPパターン(Passive View方式)を採用する。
—
2. アーキテクチャ設計:Presenterを完全に孤立させる
目指す構造はこうだ。
- View (Form): UIの入出力「だけ」を行う(ロジックを書かない)。インターフェース(`IMainView`)を実装する。
- Presenter: 画面の振る舞い、バリデーション、データ処理のフローを制御する。UIコントロールを一切知らない。知っているのは `IMainView` だけだ。
- Model: データアクセスや外部API連携を担当する。
ステップ1: Viewのインターフェース定義
Presenterが操作する画面の契約(Contract)を定義する。
Namespace Views
Public Interface IMainView
Property UserName As String
Property StatusMessage As String
Sub ShowErrorMessage(message As String)
Sub ShowSuccessMessage(message As String)
End Interface
End Namespace
ステップ2: ビジネスロジックを支配する Presenter
Presenterは、Viewのインターフェースとデータサービスを受け取る。UIの存在を一切意識しないため、単体テストが極めて容易になる。
Imports System.Data.SqlClient
Imports WinFormsApp.Views
Namespace Presenters
Public Class MainPresenter
Private ReadOnly _view As IMainView
Private ReadOnly _userRepository As IUserRepository
‘ 依存性注入(DI)を受け入れるコンストラクタ
Public Sub New(view As IMainView, userRepository As IUserRepository)
_view = view
_userRepository = userRepository
End Sub
‘ ボタンが押されたときのビジネスロジック
Public Sub OnSubmitClicked()
‘ 1. バリデーション
If String.IsNullOrWhiteSpace(_view.UserName) Then
_view.ShowErrorMessage(“名前を入力してください。”)
Return
End If
Try
‘ 2. データ永続化(Modelへ委譲)
_userRepository.SaveUser(_view.UserName.Trim())
‘ 3. 画面状態の更新
_view.StatusMessage = “保存完了”
_view.ShowSuccessMessage(“ユーザーが正常に登録されました。”)
Catch ex As Exception
‘ 異常系ハンドリング
_view.ShowErrorMessage($”予期せぬエラーが発生しました: {ex.Message}”)
End Try
End Sub
End Class
‘ リポジトリのインターフェース(テスト時にMoqでモック化する対象)
Public Interface IUserRepository
Sub SaveUser(userName As String)
End Interface
End Namespace
—
3. NUnitとMoqによる単体テストの自動化
UI要素を排除したため、Presenterのテストは純粋なロジックのテストとして高速かつ確実に実行できる。ここでMoqを使い、UIやデータベースを「偽物(モック)」に置き換えて挙動を検証する。
テストプロジェクトの構築
テストプロジェクト(NUnit Test Project)を作成し、以下のNuGetパッケージを導入する。
- `NUnit`
- `NUnit3TestAdapter`
- `Moq`
鉄壁の単体テストコード
Imports NUnit.Framework
Imports Moq
Imports WinFormsApp.Views
Imports WinFormsApp.Presenters
Namespace Tests
Public Class MainPresenterTests
Private _mockView As Mock(Of IMainView)
Private _mockRepo As Mock(Of IUserRepository)
Private _presenter As MainPresenter
Public Sub SetUp()
‘ 各テストの実行前にモックを初期化
_mockView = New Mock(Of IMainView)()
_mockRepo = New Mock(Of IUserRepository)()
‘ Presenterにモックを注入
_presenter = New MainPresenter(_mockView.Object, _mockRepo.Object)
End Sub
Public Sub UserNameが空の場合_エラーメッセージが表示され保存されないこと()
‘ Arrange(準備): ViewのUserNameプロパティが空を返すように設定
_mockView.Setup(Function(v) v.UserName).Returns(String.Empty)
‘ Act(実行): Presenterのアクションを呼び出し
_presenter.OnSubmitClicked()
‘ Assert(検証): エラー表示メソッドが呼ばれたか
_mockView.Verify(Sub(v) v.ShowErrorMessage(“名前を入力してください。”), Times.Once)
‘ リポジトリのSaveUserが一度も呼ばれていないこと
_mockRepo.Verify(Sub(r) r.SaveUser(It.IsAny(Of String)()), Times.Never)
End Sub
Public Sub 正常なUserNameの場合_リポジトリが呼ばれ成功メッセージが表示されること()
‘ Arrange
Dim testUser As String = “Taro Yamada”
_mockView.Setup(Function(v) v.UserName).Returns(testUser)
‘ Act
_presenter.OnSubmitClicked()
‘ Assert
‘ リポジトリに正しい値で保存が走ったか
_mockRepo.Verify(Sub(r) r.SaveUser(testUser), Times.Once)
‘ ステータスが更新されたか
_mockView.SetReturnsDefault(Of String)(Nothing) ‘ プロパティ設定の検証
_mockView.VerifySet(Sub(v) v.StatusMessage = “保存完了”, Times.Once)
_mockView.Verify(Sub(v) v.ShowSuccessMessage(It.IsAny(Of String)()), Times.Once)
End Sub
End Class
End Namespace
このテストを実行せよ。Windows Formsアプリでありながら、画面を1ミリも起動することなく、数ミリ秒単位でロジックの完全性が検証される。 これこそがプロフェッショナルのテスト駆動開発(TDD)だ。
—
4. CI/CDパイプラインへの統合:GitHub Actionsで完全自動化
ローカルでテストが通るだけでは不十分だ。「開発者がプッシュした瞬間に、サーバー側でビルドと全単体テストが自動実行される仕組み」を構築して初めて近代的な開発と言える。
GitHub Actionsを使用し、プルリクエストやマージの度に自動テストを実行するワークフローファイル(`.github/workflows/dotnet.yml`)を配置する。
name: .NET WinForms Unit Tests
on:
push:
branches: [ “main”, “develop” ]
pull_request:
branches: [ “main”, “develop” ]
jobs:
build-and-test:
runs-on: windows-latest # WinFormsの依存関係を解決するためWindows環境を指定
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x # またはプロジェクトのターゲットフレームワーク
- name: Restore dependencies
run: dotnet restore
- name: Build Solution
run: dotnet build –configuration Release –no-restore
- name: Run Unit Tests
run: dotnet test –configuration Release –no-build –verbosity normal
このCI/CDパイプラインを導入することで、以下のメリットがもたらされる。
1. デグレ(退行バグ)の完全封鎖: 誰かがコードを汚染しても、CIサーバーがビルドエラーやテスト失敗を検知し、マージをブロックする。
2. 心理的安全性の獲得: 「リファクタリングをしたいが、どこが壊れるか怖い」という呪縛から解放され、常にクリーンなコードベースを維持できる。
—
5. チーフアーキテクトからの最終提言
「古い技術だからテストできない」というのは、設計を怠った者たちの単なる言い訳に過ぎない。VB.NETであれ、Windows Formsであれ、「責務を分離する(Separation of Concerns)」というソフトウェア工学の鉄則さえ守れば、モダンなWebアプリケーションに負けない堅牢性とテスト容易性を手に入れることができる。
今すぐあなたのプロジェクトの `Form.vb` を開き、ビジネスロジックをPresenterへと抽出しなさい。テストコードの書かれた赤と緑のインジケーターが、あなたに本当の「開発の自由」をもたらすはずだ。
