【テクニカル・上級編】VB.NETでの単体テスト自動化:NUnit/xUnitとMoqを用いたモックオブジェクトの活用法 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

VB.NET極限開発論:外部依存を断ち切る!NUnit/xUnitとMoqによる堅牢な単体テスト自動化の極意

レガシーなVB6(VBA)システムから、現代の.NET(.NET Core / .NET 5+)環境への移行、あるいは長年稼働してきたVB.NETデスクトップアプリの保守において、最も開発者の頭を悩ませる問題は何か?
それは「DBや外部API、さらにはWin32 APIやCOMコンポーネントへの強固な結合」による、テストの困難さだ。

「テスト環境のDBが落ちているからテストできない」
「社内ネットワークのAPIに依存しているため、CI/CDパイプラインに組み込めない」
「ボタンをポチポチ押す手動テストに毎スプリント何時間も奪われている」

このような非効率な現場を救う唯一の解法が、インターフェースによる疎結合化と、Moqを用いたモックオブジェクトによる単体テストの自動化である。

本稿では、レガシーな設計思想を引きずるVB.NETコードを外科手術的にリファクタリングし、NUnit/xUnitとMoqを駆使して「外部環境に一切依存しない」極限のテスト自動化アーキテクチャを構築する知見を伝授する。

1. なぜVB.NETのテストは泥沼化するのか?

多くのVB.NETエンジニアが陥る罠は、サービスクラスの内部で直接 `New` 演算子を使い、SQL Serverへ接続したり、HTTPクライアントを生成したりすることだ。

.net
‘ 【アンチパターン】これではテスト時にDBや実APIが必須となり、CI/CDで動かない
Public Class OrderProcessor
Public Function ProcessOrder(orderId As Integer) As Boolean
‘ 内部でハードコードされたデータアクセス層を直接インスタンス化
Dim dao As New SqlOrderDao()
Dim order = dao.GetOrder(orderId)

If order Is Nothing Then Return False

order.Status = “Processed”
dao.Update(order)

‘ 外部決済APIを直接叩く
Dim api As New LegacyPaymentGateway()
Return api.Charge(order.Amount)
End Function
End Class

このコードの罪深い点は、「ビジネスロジック」「データ永続化」「外部通信」が密結合していることにある。これをテストするには、テスト用DBを構築し、外部決済APIのモックサーバーを用意し、ネットワークを疎通させる必要がある。これでは「単体テスト(Unit Test)」ではなく、極めて不安定な「結合テスト(Integration Test)」だ。

2. 依存性注入(DI)とインターフェースによる疎結合化

真の単体テストを可能にする第一歩は、「依存性の注入(Dependency Injection: DI)」である。具象クラスではなく、抽象(インターフェース)に依存させる。

以下の3ステップでアーキテクチャを再構築する。

1. データアクセスとAPI通信をインターフェース化する。
2. サービスクラスのコンストラクタで、それらのインターフェースを受け取る(コンストラクタインジェクション)。
3. テスト時には、Moqフレームワークを使ってインターフェースの振る舞いを完全に偽装(モック化)する。

リファクタリング後のドメイン・サービス層(VB.NET)

.net
Imports System

‘ 1. データアクセスのインターフェース
Public Interface IOrderDao
Function GetOrder(orderId As Integer) As Order
Sub Update(order As Order)
End Interface

‘ 2. 外部決済APIのインターフェース
IPublic Interface IPaymentGateway
Function Charge(amount As Decimal) As Boolean
End Interface

‘ 3. 注文データのエンティティ
Public Class Order
Public Property Id As Integer
Public Property Amount As Decimal
Public Property Status As String
End Class

‘ 4. 依存性を外部から注入する設計(テスト容易性が極限まで高まったサービスクラス)
Public Class OrderProcessor
Private ReadOnly _orderDao As IOrderDao
Private ReadOnly _paymentGateway As IPaymentGateway

‘ コンストラクタインジェクション
Public Sub New(orderDao As IOrderDao, paymentGateway As IPaymentGateway)
_orderDao = orderDao
_paymentGateway = paymentGateway
End Sub

Public Function ProcessOrder(orderId As Integer) As Boolean
Dim order = _orderDao.GetOrder(orderId)
If order Is Nothing Then Return False

order.Status = “Processed”
_orderDao.Update(order)

Return _paymentGateway.Charge(order.Amount)
End Function
End Class

3. NUnit / xUnit と Moq を用いたテスト自動化の実装

アーキテクチャが整えば、あとはテストコードを書くだけだ。ここでは現代の.NETエコシステムにおけるデファクトスタンダードである xUnit(またはNUnit)と、モックライブラリ Moq をVB.NETで駆使する実例を示す。

テストプロジェクトのセットアップ(NuGetパッケージ)

  • `xunit` (または `NUnit`)
  • `xunit.runner.visualstudio`
  • `Moq`

ユニットテストコードの実装

外部DBもAPIも不要。メモリ上で完結し、数ミリ秒で実行される完璧な単体テストコードを記述する。

.net
Imports Xunit
Imports Moq

Public Class OrderProcessorTests


Public Sub ProcessOrder_Should_Process_And_Charge_Successfully()
‘ — 1. 準備 (Arrange) —

‘ IOrderDaoのモックを作成
Dim mockDao As New Mock(Of IOrderDao)()
Dim testOrder As New Order With {.Id = 1, .Amount = 1000D, .Status = “Pending”}

‘ GetOrderが呼ばれたらtestOrderを返すようにセットアップ
mockDao.Setup(Function(d) d.GetOrder(1)).Returns(testOrder)

‘ Updateが呼ばれた時は何もせずスルー(必要に応じてCallbackやVerifiableを設定)
mockDao.Setup(Sub(d) d.Update(It.IsAny(Of Order)())).Verifiable()

‘ IPaymentGatewayのモックを作成
Dim mockGateway As New Mock(Of IPaymentGateway)()
‘ Chargeがどんな金額であれTrueを返すようにセットアップ
mockGateway.Setup(Function(g) g.Charge(1000D)).Returns(True)

‘ テスト対象のクラスにモックを注入
Dim processor As New OrderProcessor(mockDao.Object, mockGateway.Object)

‘ — 2. 実行 (Act) —
Dim result As Boolean = processor.ProcessOrder(1)

‘ — 3. 検証 (Assert) —
Assert.True(result)
Assert.Equal(“Processed”, testOrder.Status)

‘ モックされたメソッドが意図通りに呼び出されたかを厳密に検証
mockDao.Verify(Sub(d) d.Update(It.IsAny(Of Order)()), Times.Once())
mockGateway.Verify(Function(g) g.Charge(1000D), Times.Once())
End Sub


Public Sub ProcessOrder_Should_Return_False_When_Order_Not_Found()
‘ — 1. 準備 (Arrange) —
Dim mockDao As New Mock(Of IOrderDao)()
‘ 存在しないIDの場合はNothingを返す
mockDao.Setup(Function(d) d.GetOrder(999)).Returns(CType(Nothing, Order))

Dim mockGateway = New Mock(Of IPaymentGateway)()

Dim processor As New OrderProcessor(mockDao.Object, mockGateway.Object)

‘ — 2. 実行 (Act) —
Dim result As Boolean = processor.ProcessOrder(999)

‘ — 3. 検証 (Assert) —
Assert.False(result)

‘ 決済APIが一切呼ばれていないことを保証する
mockGateway.Verify(Function(g) g.Charge(It.IsAny(Of Decimal)()), Times.Never())
End Sub

End Class

4. レガシー環境・WinAPI・COM連携における「モック化の極意」

実務の現場では、純粋なドメインロジックばかりではない。「レガシーなWindows API呼び出し」や「COMコンポーネント(Excel操作やレジストリ操作など)」がビジネスロジックに絡んでいるケースに直面する。

これらをそのままテストコードに書くと、ビルドサーバー(DockerコンテナやGitHub Actions等のLinux/Windows環境)で例外が爆発する。ここでもアプローチは同じだ。「OSや外部リソースに依存する部分」をインターフェースでラップし、テスト時はモックに置き換える。

例:ファイルシステム・OS操作の抽象化

.net
‘ OSに依存する処理のインターフェース
Public Interface ISystemWrapper
Function GetMachineName() As String
Function FileExists(path As String) As Boolean
End Interface

‘ 本番用の具象クラス(実際のWin32 APIやSystem.IOを叩く)
Public Class RealSystemWrapper
Implements ISystemWrapper

Public Function GetMachineName() As String Implements ISystemWrapper.GetMachineName
Return Environment.MachineName
End Function

Public Function FileExists(path As String) As Boolean Implements ISystemWrapper.FileExists
Return System.IO.File.Exists(path)
End Function
End Class

ビジネスロジック側では `Environment.MachineName` を直接書くのではなく、`ISystemWrapper` をインジェクションして呼び出す。これにより、テスト時は「特定のマシン名の場合の挙動」「ファイルが存在する場合・しない場合の分岐」をMoqによって自在にシミュレートできるようになる。

5. チーフアーキテクトからの提言:CI/CDパイプラインとの統合

単体テストコードを書くだけで満足してはならない。真の自動化とは、「開発者がコードをプッシュした瞬間、GitHub ActionsやAzure DevOpsなどのCI/CDパイプライン上で、数秒〜数分で全テストが自動実行される状態」を指す。

外部DBや実APIに依存したテストコードは、CI/CD環境の構築コストを跳ね上げ、ビルドを不安定にする(Flaky Testsの温床となる)。
Moqを活用したモックテストを徹底し、「IOを伴う処理は境界線の外側に追いやり、コアロジックは完全なメモリ内テストで網羅する」という原則をチーム全体に徹底せよ。

VB.NETは、決して「古い言語」ではない。設計の近代化(Modern .NET Architecture)を受け入れる器量を持った、極めて実用的な言語である。レガシーの呪縛を断ち切り、テスト駆動による圧倒的なコード品質を手に入れろ。

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