【VBA】密結合コードが原因で単体テストが書けない!テスト容易性を下げるアンチパターンとリファクタ術

VBAコードの密結合を解消し単体テストを可能にするリファクタリングとテスト容易性向上の解説記事のアイキャッチ画像 アーキテクチャ

VBAで開発を行っていると、ついつい手続き型の巨大なSubプロシージャを書いてしまいがちです。
そのようなコードは、データベースアクセスやUI操作、ビジネスロジックが一つの関数内に混在する密結合状態に陥っています。
ソフトウェア工学の観点から見れば、これはテスト容易性を著しく低下させる典型的なアンチパターンです。

単体テストを導入しようとしても、密結合なコードではテスト対象を孤立させることができず、検証が困難になります。
具体的には以下のような問題が顕在化します。

アンチパターン 影響 テスト容易性への障害
巨大なSubプロシージャ 責務の混在 テスト対象範囲が曖昧になる
外部リソースへの直接アクセス 副作用の発生 DB等の事前準備が必須となる
戻り値の欠如 状態の外部出力 結果の自動検証が不可能になる

これでは自動化されたフィードバックループを回すことは不可能です。
本記事では、VBAコードにおける密結合のアンチパターンを整理し、単体テストが書ける構造へとリファクタリングする手法を解説します。

具体的には、責務の分離と依存性の注入の考え方を用いて、副作用を持つ処理と純粋なロジックを切り離すアプローチを示します。
レガシーコードを保守可能な状態へと変貌させるための実践的な知見を得るために、ぜひ最後までお読みください。

VBAにおける単体テストの重要性と密結合の現状

VBAの単体テストの重要性と密結合コードの問題点を解説する図解

VBA(Visual Basic for Applications)は業務自動化のツールとして広く利用されていますが、コンピュータサイエンスの観点から見ると、そのコード品質は必ずしも適切に管理されているとは限りません。
特に単体テストの欠如は、システムの規模が拡大するにつれて保守コストを肥大化させる要因となります。
本記事では、VBA開発において単体テストが行われない現状を整理し、その根本原因を論理的に分析します。

なぜVBA開発で単体テストが行われないのか

VBA開発において単体テストが浸透しない理由は、主に開発環境の特性と歴史的背景に起因します。
第一に、VBAには近代的なプログラミング言語のような標準的なテストフレームワークが標準で組み込まれておらず、テスト環境の構築自体に初期コストがかかります。
また、マクロの記録機能による利便性が、逆に非構造化なコードをそのまま資産として残存させる結果を招いています。

より本質的な問題は、ビジネスロジックとインフラストラクチャ層(ExcelのUI操作やファイルI/Oなど)が密結合している点です。
テスト対象のモジュールがUI操作やデータベースアクセスといった副作用を伴う処理を直接実行している場合、そのテストを実行するためには実際のExcelファイルやデータベース環境を用意しなければなりません。
このような環境への依存はテストの実行コストを跳ね上げ、高速かつ自動化されたフィードバックループを構築することを困難にします。

結果として、開発者は短期的な動作確認を優先し、テストコードによる安全網の構築を後回しにする傾向が強まります。

密結合コードが引き起こす保守性の低下

テスト容易性が低下する根本的な原因は、密結合なコード構造にあります。
密結合とは、複数の責務が一つのモジュールやプロシージャ内で強く依存し合っている状態を指し、ソフトウェア工学においては結合度が高く、内包度が低いアンチパターンとされます。

以下のコードは、VBAで頻出する典型的な密結合のアンチパターンです。

Sub ProcessData()
    Dim ws As Worksheet
    Set ws = ThisWorkbook.Sheets("Data")

    Dim i As Integer
    Dim val As Integer
    For i = 1 To 10
        val = ws.Cells(i, 1).Value
        If val > 100 Then
            ws.Cells(i, 2).Value = "High"
        Else
            ws.Cells(i, 2).Value = "Low"
        End If
    Next i
End Sub

このプロシージャでは、特定のシートからのデータ読み込み、閾値判定のビジネスロジック、そしてセルへの書き込みという異なる責務が混在しています。
この処理を単体テストするためには、物理的に「Data」というシートと入力データが存在しなければならず、ロジックの検証のみを切り出して実行することが不可能です。

密結合が保守性に与える具体的な悪影響は、以下の要因として顕在化します。

要因 説明 保守性への影響
責務の混在 I/O処理とビジネスロジックが同一手続き内に存在 修正の影響範囲が拡大し、回帰テストが困難になる
副作用への依存 実行順序や外部状態が処理結果を左右する 実行状態の確定性が失われ、テストの再現性が低下する
強固なオブジェクト参照 WorkSheetやRangeオブジェクトを直接操作 モックへの置き換えが不可能で、環境依存度が高まる

このように、密結合なコードはテスト対象を孤立させることができず、自動テストの導入を阻害します。
単体テストを実施可能な状態にするためには、これらの結合を解消し、副作用を持つ処理と純粋なロジックを明確に分離する設計が不可欠です。

テスト容易性を阻害するVBAのアンチパターンとは

VBAのテスト容易性を下げるアンチパターンの具体例と解説

ソフトウェア工学の観点において、テスト容易性はコードの品質を測る重要な指標です。
しかし、VBA開発では特有の記述傾向により、このテスト容易性が意図せず低下することが多々あります。
ここでは、単体テストの導入を困難にする代表的な3つのアンチパターンを挙げ、その技術的な課題を分析します。

巨大なSubプロシージャによる責務の混在

オブジェクト指向設計の基本原則である単一責任の原則(SRP)がVBAでは軽視されがちです。
一つのSubプロシージャが数十行から数百行に及び、データの入力、加工、出力といった複数の責務を抱え込む「神プロシージャ」と化しているケースをよく見かけます。

このような巨大なプロシージャは、テスト対象を局所化することを不可能にします。
例えば、以下のコードを見てください。

Sub GenerateMonthlyReport()
    Dim rs As Object
    Set rs = CreateObject("ADODB.Recordset")
    rs.Open "SELECT * FROM Sales", "Provider=SQLOLEDB;Data Source=...;Initial Catalog=DB;User ID=user;Password=pass;"

    Dim total As Double
    total = 0
    Do While Not rs.EOF
        total = total + rs.Fields("Amount").Value
        rs.MoveNext
    Loop

    Dim ws As Worksheet
    Set ws = ThisWorkbook.Sheets("Report")
    ws.Cells(1, 1).Value = "Total Sales"
    ws.Cells(1, 2).Value = total

    Dim outlookApp As Object
    Set outlookApp = CreateObject("Outlook.Application")
    Dim mail As Object
    Set mail = outlookApp.CreateItem(0)
    mail.To = "manager@example.com"
    mail.Subject = "Monthly Report"
    mail.Body = "Total: " & total
    mail.Send
End Sub

このプロシージャは、データベースへの接続、売上の集計計算、Excelシートへの出力、そしてメール送信という4つの異なる責務を担っています。
この処理に対する単体テストを書こうとした場合、データベースの可用性やOutlookの環境設定に依存せざるを得ず、純粋なロジックの検証だけを切り出すことができません。

外部リソースへの直接依存と副作用

前節のコード例にも関連しますが、モジュール内で外部リソース(データベース、ファイルシステム、他のアプリケーションオブジェクト)を直接インスタンス化して操作することも、テスト容易性を著しく損なうアンチパターンです。

直接依存が生じると、コードの実行において副作用が不可避となります。
副作用とは、関数の戻り値以外の形で外部の状態を変化させる作用を指します。
外部リソースを直接操作するコードは、実行されるたびにファイルの作成やデータベースのレコード更新を行うため、テストの何度実行しても同じ結果になるという性質を保証できません。

また、外部環境の状態によって結果が変わるため、テストが非決定的になります。
単体テストは隔離された環境で高速に実行されるべきですが、外部リソースへの直接依存はその前提を根底から覆してしまいます。

戻り値を持たない状態変更の連鎖

VBAでは戻り値を返さないSubプロシージャが多用される傾向があります。
Subプロシージャ自体が悪なわけではありませんが、状態の変更を行うSubプロシージャが連鎖して呼び出される構造は、状態遷移の追跡を困難にします。

良い設計では、状態を変更するコマンドと、状態を取得するクエリを分離するCQRS(Command Query Responsibility Segregation)の考え方が重要です。
しかし、状態変更を連鎖させるコードでは、テスト実行後の結果検証が非常に難しくなります。

Sub UpdateInventory(ByRef product As Object)
    product.Stock = product.Stock - 1
    If product.Stock < 10 Then
        ReorderProduct product
    End If
End Sub
Sub ReorderProduct(ByRef product As Object)
    product.OrderPending = True
    LogReorderRequest product.ID
End Sub

この例では、UpdateInventoryが引数として渡されたオブジェクトの状態を変更し、さらに別のプロシージャReorderProductを呼び出して別の状態変更を引き起こします。
この一連の処理をテストするには、初期状態のオブジェクトを用意し、一連の呼び出し後にオブジェクトが期待される状態になっているかを検証しなければなりません。

アンチパターン テスト容易性への影響 解決の方向性
巨大なSubプロシージャ 責務が混在し、部分的なテストが不可能 責務ごとにプロシージャを分割
外部リソースへの直接依存 副作用が生じ、非決定的なテストになる 依存性の注入でモック可能にする
戻り値のない状態変更 状態遷移の追跡と検証が困難になる 純粋関数化と状態のカプセル化

これらのアンチパターンを放置したままでは、保守性の高いコードを維持することは不可能です。
次のステップでは、これらの密結合を解消するための具体的なアプローチを検討します。

VBAコードの依存関係を整理するための分析手法

VBAコードの依存関係を整理し分析するための手法を解説

レガシーコードのリファクタリングにおいて、闇雲にコードを書き換えることは甚大なリスクを伴います。
特にVBAのような動的型付けに近い環境では、コンパイラによる厳密な依存関係のチェックが期待できないため、事前にコードベースの依存関係を静的に分析し、どこに結合度の高い箇所が存在するかを特定することが不可欠です。
本セクションでは、ソフトウェア工学の知見をベースに、VBAコードの複雑性を紐解くための具体的な分析手法を解説します。

影響スケッチによる依存関係の可視化

アーキテクチャの解析において、コンポーネント間のデータと制御の流れを視覚化する手法は非常に有効です。
影響スケッチは、あるモジュールやプロシージャが変更された際に、どの範囲に影響が波及するかをマッピングする手法です。

VBAプロジェクトにおいて、この手法を適用するための具体的なステップは以下の通りです。

  1. エントリポイントとなるマクロやイベントハンドラを特定する
  2. そこから呼び出されるプロシージャの呼び出しグラフ(Call Graph)を描画する
  3. 各プロシージャが参照・更新するグローバル変数や外部リソース(セル、DB、ファイル)を矢印で結ぶ

この分析を行うことで、単純な計算ロジックであるはずの関数が、予期せずワークシートの状態を参照している「隠れた依存」や、複数のプロシージャが同一のグローバル変数を非決定的に更新している「データ結合」の箇所を浮き彫りにできます。
影響の波及範囲が広いノードこそが、リファクタリングの優先対象となります。

副作用と純粋関数の境界線の引き方

依存関係を可視化した後は、コードを「副作用を持つ処理」と「純粋な計算を行う処理」に分離します。
ソフトウェア工学において、同じ入力に対して常に同じ出力を返し、外部の状態を一切変更しない関数を純粋関数と呼びます。
純粋関数は単体テストが極めて容易です。

VBAのレガシーコードでは、計算処理の途中でセルに直接値を書き込んだり、データベースを更新したりする副作用が混在しています。
この境界線を引くためには、関数の入力と出力を明示的に定義し直す必要があります。

以下は、税込価格を計算しながら同時にセルの背景色を変更する、副作用を含んだアンチパターンのコードです。

Sub CalculateTotalWithTax()
    Dim ws As Worksheet
    Set ws = ThisWorkbook.Sheets("Sales")

    Dim price As Double
    price = ws.Range("B2").Value

    Dim taxRate As Double
    taxRate = 0.1

    Dim total As Double
    total = price + (price * taxRate)

    ws.Range("C2").Value = total
    If total > 10000 Then
        ws.Range("C2").Interior.Color = RGB(255, 0, 0)
    End If
End Sub

この処理から純粋関数を抽出し、副作用の境界線を明確に引き直したのが以下のコードです。

' 純粋関数:外部状態に依存しない計算のみを行う
Function CalculateTotal(price As Double, taxRate As Double) As Double
    CalculateTotal = price + (price * taxRate)
End Function
' 副作用を持つプロシージャ:UI操作と関数の呼び出しを担当
Sub UpdateSalesSheet()
    Dim ws As Worksheet
    Set ws = ThisWorkbook.Sheets("Sales")

    Dim price As Double
    price = ws.Range("B2").Value

    Dim total As Double
    ' 純粋関数を呼び出して計算
    total = CalculateTotal(price, 0.1)

    ' 副作用(UI更新)の実行
    ws.Range("C2").Value = total
    If total > 10000 Then
        ws.Range("C2").Interior.Color = RGB(255, 0, 0)
    End If
End Sub

このように分離することで、CalculateTotal関数は単体テストフレームワーク上で引数と戻り値のみを検証すればよくなり、Excelシートの準備や後片付けが不要になります。

特徴 副作用を持つプロシージャ 純粋関数
外部状態の参照 セルやグローバル変数を参照する 引数のみを参照する
状態の変更 セルやDBの値を更新する 戻り値として結果を返すのみ
テスト容易性 環境構築が必要で困難 単体テストが容易かつ高速

境界線を引く基本原則は、「データを取得する」「計算する」「結果を保存する」という3つのフェーズにプロシージャを分割することです。
この分離により、VBAコードのテスト容易性は飛躍的に向上します。

密結合を解消するリファクタリングの基本戦略

VBAの密結合を解消するためのリファクタリング基本戦略

ソフトウェアの保守性を向上させるためには、モジュール間の結合度を下げ、内聚度を高めることが不可欠です。
VBAのレガシーコードにおいて密結合を解消するための基本戦略は、システムの関心の分離を行うことにあります。
具体的には、ビジネスルールを評価するロジックと、外部環境に作用するインフラストラクチャ層の処理を明確に分離するアプローチをとります。
ここでは、その中でも最も効果的な二つのリファクタリング戦略を解説します。

ビジネスロジックとUI操作の分離

VBA開発において最も頻発する問題は、ワークシートの操作(UI層)と業務計算(ビジネスロジック層)が同一のSubプロシージャ内に混在していることです。
ソフトウェアアーキテクチャの観点から言えば、UIの変更がビジネスロジックに影響を与える構造は、脆い設計と言わざるを得ません。

この問題を解消するには、ビジネスロジックを純粋なFunctionプロシージャとして抽出し、UI操作から完全に切り離す必要があります。
以下のアンチパターンは、顧客ランクに応じた割引率を適用しながら、直接セルに結果を書き込んでいます。

Sub ApplyDiscountAndWriteCell()
    Dim ws As Worksheet
    Set ws = ThisWorkbook.Sheets("Customers")

    Dim totalAmount As Double
    totalAmount = ws.Range("B2").Value

    Dim customerRank As String
    customerRank = ws.Range("C2").Value

    ' ビジネスロジックとUI操作の混在
    Dim finalPrice As Double
    If customerRank = "A" Then
        finalPrice = totalAmount * 0.9
    ElseIf customerRank = "B" Then
        finalPrice = totalAmount * 0.95
    Else
        finalPrice = totalAmount
    End If

    ws.Range("D2").Value = finalPrice
End Sub

この処理をリファクタリングし、ビジネスロジックをUIから分離すると以下のようになります。

' ビジネスロジック層:UIに依存しない純粋な関数
Function CalculateDiscountedPrice(amount As Double, rank As String) As Double
    Select Case rank
        Case "A"
            CalculateDiscountedPrice = amount * 0.9
        Case "B"
            CalculateDiscountedPrice = amount * 0.95
        Case Else
            CalculateDiscountedPrice = amount
    End Select
End Function
' UI操作層:データの入出力と関数の呼び出しのみを担当
Sub UpdateCustomerSheet()
    Dim ws As Worksheet
    Set ws = ThisWorkbook.Sheets("Customers")

    Dim amount As Double
    Dim rank As String
    amount = ws.Range("B2").Value
    rank = ws.Range("C2").Value

    ' ロジックの実行
    Dim finalPrice As Double
    finalPrice = CalculateDiscountedPrice(amount, rank)

    ' 結果の書き込み
    ws.Range("D2").Value = finalPrice
End Sub

このように分離することで、CalculateDiscountedPrice関数はExcelシートの存在を知る必要がなくなり、単体テストでは引数として様々な金額とランクの組み合わせを渡すだけで網羅的な検証が可能になります。

データベースアクセスの抽象化

次に重要なのは、ADO(ActiveX Data Objects)を用いたデータベースアクセスの抽象化です。
SQLクエリの実行や接続文字列の管理がビジネスロジック内に直接記述されていると、データベースサーバーの変更やモックの利用が困難になります。

この問題に対処するためには、データアクセスオブジェクト(DAO)パターンやリポジトリパターンを適用し、データ永続化の詳細をクラスモジュールの背後に隠蔽します。
VBAではImplementsキーワードを用いてインターフェースを定義することで、依存性逆転の原則(DIP)に則った設計が可能です。

まず、データアクセスの契約となるインターフェース(クラスモジュール:IDbRepository)を定義します。

' クラスモジュール: IDbRepository
Public Function GetUserName(userId As String) As String
End Function

次に、このインターフェースを実装した具象クラス(クラスモジュール:SqlDbRepository)を作成し、実際のデータベースアクセスをカプセル化します。

' クラスモジュール: SqlDbRepository
Implements IDbRepository
Private Function IDbRepository_GetUserName(userId As String) As String
    Dim conn As Object
    Set conn = CreateObject("ADODB.Connection")
    conn.Open "Provider=SQLOLEDB;Data Source=...;Initial Catalog=...;User ID=...;Password=..."

    Dim rs As Object
    Set rs = conn.Execute("SELECT UserName FROM Users WHERE ID = '" & userId & "'")

    If Not rs.EOF Then
        IDbRepository_GetUserName = rs.Fields("UserName").Value
    Else
        IDbRepository_GetUserName = "Unknown"
    End If

    rs.Close
    conn.Close
End Function

ビジネスロジック側では、具象クラスではなくインターフェース(IDbRepository)に依存するように記述します。
これにより、テスト時にはデータベースにアクセスしないダミーのモックオブジェクト(MockDbRepository)を注入することが可能になります。

リファクタリング戦略 分離前の状態 リファクタリング後の状態 テスト容易性の向上点
UI操作の分離 計算式がワークシート操作に埋め込まれている 純粋関数として計算ロジックを抽出 Excelファイルなしで計算ロジックを検証可能
DBアクセスの抽象化 SQLクエリと接続処理が直接記述されている リポジトリパターンでインターフェース化 モックオブジェクトによりDB不要でテスト可能

これらの基本戦略を実践することで、VBAコードの依存関係は劇的に整理され、自動テストが可能な状態へと近づいていきます。

VBAにおける依存性の注入(DI)の実装アプローチ

VBAで依存性の注入を実装しテスト容易性を高めるアプローチ

ソフトウェア工学において、依存性の注入(Dependency Injection: DI)は、モジュール間の結合度を下げるための重要なデザインパターンです。
VBAには近代的な言語のようなDIコンテナが存在しませんが、オブジェクト指向の原則を適用することで、このパターンを手動で実装可能です。
DIを導入することで、テスト対象のクラスが外部リソースに直接依存することを防ぎ、単体テスト時にはその依存先を自由に差し替えられるようになります。
本セクションでは、VBAにおけるDIの具体的な実装アプローチを解説します。

クラスモジュールを用いたインターフェースの定義

VBAでDIを実現する第一歩は、クラスモジュールを用いたインターフェースの定義です。
VBAのImplementsキーワードを利用することで、クラス間の契約(プロトコル)を明示的に定義できます。
これにより、呼び出し側は具象クラスではなくインターフェースに依存するようになり、依存関係逆転の原則(DIP)に則った設計が可能となります。

例として、外部APIから為替レートを取得するサービスを想定します。
まず、為替レート取得の契約となるインターフェースを定義します。

' クラスモジュール: IExchangeRateService
Public Function GetRate(currencyCode As String) As Double
End Function

次に、このインターフェースを実装した本番環境用の具象クラスを作成します。
HTTPリクエストなどの副作用を伴う処理は、このクラス内にカプセル化します。

' クラスモジュール: HttpExchangeRateService
Implements IExchangeRateService
Private Function IExchangeRateService_GetRate(currencyCode As String) As Double
    ' 実際にはMSXML2.XMLHTTPなどを用いてAPI通信を行う
    ' ここでは副作用を示すため、擬似的な固定値を返す
    If currencyCode = "USD" Then
        IExchangeRateService_GetRate = 150.0
    Else
        IExchangeRateService_GetRate = 1.0
    End If
End Function

テスト対象となるビジネスロジック側は、このIExchangeRateServiceインターフェースに依存するように設計します。
VBAにはコンストラクタ引数が存在しないため、プロパティまたは初期化用メソッド(Initメソッド)を通じて依存性を注入します。

' クラスモジュール: OrderCalculator
Private rateService As IExchangeRateService
' 依存性の注入用メソッド
Public Sub Init(service As IExchangeRateService)
    Set rateService = service
End Sub
Public Function CalculateYenTotal(usdAmount As Double) As Double
    Dim rate As Double
    rate = rateService.GetRate("USD")
    CalculateYenTotal = usdAmount * rate
End Function

この設計により、OrderCalculatorは通信処理の詳細を知る必要がなくなり、為替レートの計算ロジックにのみ集中できるようになります。

テストダブルを活用したモックの作成

インターフェースを通じた依存関係が構築できれば、次は単体テスト用のテストダブル(テスト代役)を作成します。
テストダブルの中でも、特定の振る舞いを模倣するモックオブジェクトを作成することで、ネットワーク接続なしにビジネスロジックの検証が可能になります。

先ほど定義したIExchangeRateServiceインターフェースを実装した、テスト用のモッククラスを作成します。

' クラスモジュール: MockExchangeRateService
Implements IExchangeRateService
' テスト用に固定の値を返すように設定
Private Function IExchangeRateService_GetRate(currencyCode As String) As Double
    If currencyCode = "USD" Then
        IExchangeRateService_GetRate = 149.5 ' テスト期待値
    Else
        IExchangeRateService_GetRate = 1.0
    End If
End Function

単体テストの実行時には、本番用のHttpExchangeRateServiceではなく、このMockExchangeRateServiceOrderCalculatorに注入します。
これにより、テストはAPIサーバーの状態に左右されることなく、常に安定した結果を得ることができます。

' 標準モジュール: UnitTests
Sub TestCalculateYenTotal()
    ' Arrange(準備)
    Dim mockService As New MockExchangeRateService
    Dim calculator As New OrderCalculator
    calculator.Init mockService ' モックの注入

    ' Act(実行)
    Dim result As Double
    result = calculator.CalculateYenTotal(100.0)

    ' Assert(検証)
    ' 100.0 * 149.5 = 14950.0 であることを確認
    Debug.Assert result = 14950.0
End Sub

DIとテストダブルを組み合わせることで、テストの確定性が保証され、高速なフィードバックループが完成します。

コンポーネント 役割 テストにおける必要性
インターフェース 依存関係の契約を定義する モックへの差し替えを可能にする
具象クラス 外部リソースへの実際のアクセスを担う テスト時には使用しない
モッククラス 固定値や期待される振る舞いを返す 副作用を排除し、ロジックを検証する

VBA環境においても、オブジェクト指向の原則を適切に活用すれば、近代的なテスト手法を導入することが十分に可能です。

リファクタリング後のVBAコードに対する単体テストの実践

リファクタリング後のVBAコードに対する単体テストの実践手法

これまでに解説したリファクタリング手法を用いて密結合を解消し、純粋関数と副作用を持つ処理を分離できれば、次はいよいよ単体テストの実装フェーズに入ります。
単体テストは、コードが仕様を満たしていることを継続的に保証するための安全網であり、レガシーコードの近代化において不可欠なプロセスです。
本セクションでは、VBA環境において実際にテストコードを記述し、自動化された検証サイクルを構築する具体的な手法を解説します。

Rubberduck VBAを用いたテスト駆動開発

VBAには標準でテストフレームワークが搭載されていませんが、オープンソースのVBE(Visual Basic Editor)アドインであるRubberduck VBAを導入することで、近代的なテスト駆動開発(TDD)を実践できるようになります。
Rubberduckは、xUnit系のアサーション機能やテストエクスプローラーを提供しており、テストの実行と結果の可視化を強力にサポートします。

Rubberduckを用いたテストコードは、@TestMethodアノテーション(属性)を付与したSubプロシージャとして記述します。
以下は、メールアドレスのバリデーションを行う純粋関数IsValidEmailを検証するテストコードの例です。

'@TestMethod
Public Sub IsValidEmail_ValidFormat_ReturnsTrue()
    ' Arrange
    Dim email As String
    email = "test@example.com"

    ' Act
    Dim result As Boolean
    result = IsValidEmail(email)

    ' Assert
    Assert.IsTrue result
End Sub

このように、Arrange(準備)、Act(実行)、Assert(検証)という3Aの原則に従ってテストを記述することで、テストの意図が明確になります。
Rubberduckのテストエクスプローラーからこれを実行すると、期待通りにTrueが返されるかが自動的に検証されます。
これにより、開発者は手動でマクロを実行して目視で結果を確認する非効率なプロセスから解放されます。

純粋関数の振る舞い検証の自動化

リファクタリングによって抽出された純粋関数は、入力に対する出力が常に一定であるため、テストケースを容易に網羅できます。
特に、境界値分析や同値分割といったソフトウェアテストの技法を適用しやすいのが特徴です。

複数の入力パターンを効率的に検証するためには、パラメタライズドテスト(パラメータ化テスト)の考え方を取り入れることが有効です。
VBAでは言語機能として直接サポートされていませんが、配列を用いてテストデータを反復処理することで、同様の効果を得ることができます。
以下のテストデータを検証することを想定します。

テストケース名 入力文字列 期待される戻り値
正常なフォーマット user@domain.com True
アットマークなし userdomain.com False
ドメイン部が不正 user@.com False

これらのテストケースをループ処理で一括検証するテストコードは以下のようになります。

'@TestMethod
Public Sub IsValidEmail_VariousPatterns_ReturnsExpectedResult()
    ' Arrange
    Dim testCases As Variant
    testCases = Array( _
        Array("user@domain.com", True), _
        Array("userdomain.com", False), _
        Array("user@.com", False) _
    )

    Dim i As Integer
    For i = LBound(testCases) To UBound(testCases)
        Dim inputEmail As String
        inputEmail = testCases(i)(0)

        Dim expected As Boolean
        expected = testCases(i)(1)

        ' Act
        Dim actual As Boolean
        actual = IsValidEmail(inputEmail)

        ' Assert
        Assert.AreEqual expected, actual, "Failed on case: " & inputEmail
    Next i
End Sub

このアプローチを用いれば、テストケースが増えても配列の要素を追加するだけで対応でき、テストコードの保守性が向上します。
純粋関数は状態を持たないため、テストの実行順序に依存せず、何度実行しても同じ結果を返します。
このべき等性により、CI/CDパイプラインのような自動化プロセスにテストを組み込む際も、安定したフィードバックを得ることが可能になります。

自動テストの導入は、単なるバグの発見だけでなく、コードが仕様通りに振る舞い続けていることを保証する生きたドキュメントとしての役割も果たします。
リファクタリング後のVBAコードに対してこのようなテストを網羅することで、長期的な保守性と品質担保が劇的に向上します。

レガシーVBAを段階的に近代化するための留意点

レガシーVBAコードを段階的に近代化する際の重要な留意点

レガシーコードの近代化は、一度の巨視的な書き換えで達成されるものではありません。
ビッグバンリファクタリングは、予期せぬデグレード(回帰バグ)を引き起こすリスクが極めて高いです。
ソフトウェア工学のベストプラクティスに則り、安全網を構築しながら段階的にコードを改善していくアプローチが不可欠です。
本セクションでは、VBA特有の制約を考慮した安全なリファクタリング手順と、将来的な継続的インテグレーション(CI)への移行パスについて考察します。

既存ロジックを破壊しない安全なリファクタリング手順

リファクタリングの鉄則は、「テストがないコードはリファクタリングしない」ことです。
しかし、VBAのレガシーコードにはテストが存在しないことが前提となっています。
このジレンマを解消するためには、特徴付けテスト(Characterization Test)と呼ばれる手法を用いて、現状の振る舞いをロックダウンする必要があります。

特徴付けテストは、コードが「正しいかどうか」ではなく「現状どう動いているか」を記録するテストです。
以下のステップで安全なリファクタリングを進めます。

  1. 既存のプロシージャに対して、現状の出力結果や状態変化を観察し、それを期待値としたテストを記述する
  2. テストがパスすることを確認し、安全網を構築する
  3. 小さな単位で純粋関数を抽出し、元のプロシージャからその関数を呼び出すように変更する
  4. テストを再実行し、振る舞いが変わっていないことを検証する

例えば、以下のように元のプロシージャ内で計算処理を関数として切り出し、逐次検証を行います。

' 元の巨大なプロシージャ内の書き換え前
' total = price - (price * 0.1)
' 切り出した純粋関数
Function CalculateTax(price As Double) As Double
    CalculateTax = price * 0.1
End Function
' 書き換え後(テストで振る舞いの同一性を担保しつつ置換)
' total = price - CalculateTax(price)

このサイクルを細かく繰り返すことで、既存のビジネスロジックを破壊することなく、内部構造を安全に改善していくことができます。

フェーズ 実施内容 リスク管理の観点
觀察 現状の入出力を特定しテスト化 デグレード検知の安全網を構築
抽出 ロジックの一部を関数化 変更範囲を局所化し影響を最小化
検証 テストを再実行して結果を比較 振る舞いの保持を機械的に保証
### 継続的インテグレーションへの道筋

リファクタリングと単体テストの導入が進んだら、次はそれらを継続的インテグレーション(CI)パイプラインに組み込むことを目指します。
しかし、VBAはホストアプリケーション(Excelなど)の内部で実行されるスクリプト言語であるため、Gitなどのバージョン管理システムやCIツールとの親和性が低いという技術的な障壁があります。

この課題を解決するための道筋として、まずVBAのソースコードをテキストファイルとしてエクスポートし、バージョン管理下に置くアプローチが有効です。
VBEの設定で「ロックして表示不可」のチェックを外すことで、VBProjectオブジェクトからコードを抽出できます。

以下は、標準モジュールをエクスポートするスクリプトの雛形です。

Sub ExportAllModules()
    Dim vbComp As Object
    Dim exportPath As String
    exportPath = "C:\Repo\src\"

    For Each vbComp In ThisWorkbook.VBProject.VBComponents
        Select Case vbComp.Type
            Case 1 ' 標準モジュール
                vbComp.Export exportPath & vbComp.Name & ".bas"
            Case 2 ' クラスモジュール
                vbComp.Export exportPath & vbComp.Name & ".cls"
        End Select
    Next vbComp
End Sub

これをビルドスクリプト化し、Gitリポジトリにコミットすることで、コードの変更履歴を追跡できるようになります。
さらに発展させるには、CI環境(GitHub Actionsなど)上でExcelをCOMインターフェース経由で起動し、RubberduckのAPIを叩いてテストを自動実行する仕組みを構築します。

一足飛びに完全なCI環境を構築するのは困難ですが、まずは「ソースコードの外部ファイル化」と「ローカルでのテスト自動実行」を実現するだけでも、チーム開発における品質担保のレベルは飛躍的に向上します。
アーキテクチャの近代化は、このような小さな自動化の積み重ねによって達成されるのです。

VBAコードのテスト容易性を高め保守性を向上させるまとめ

VBAコードのテスト容易性を高め保守性を向上させる要点のまとめ

VBA(Visual Basic for Applications)は、その歴史的背景と手軽さゆえに、しばしば構造化されていないスパゲッティコードを生み出す温床となってきました。
しかし、本記事で解説したように、コンピュータサイエンスの普遍的な原則を適用することで、VBAであっても近代的なソフトウェア設計を実現し、高いテスト容易性と保守性を獲得することが可能です。

テストが書けないコードの根本原因は、常に密結合にあります。
データベースアクセス、UI操作、そしてビジネスロジックが一つのSubプロシージャ内に混在するアンチパターンは、モジュールの責務を曖昧にし、副作用を不可避なものにします。
この状態では、コードの変更は常にシステム全体へ予測不能な影響を与える危険性を孕んでおり、開発者は変更を恐れるようになります。

保守性を向上させるための第一歩は、コードの解析と責務の分離です。
影響スケッチを用いて依存関係を可視化し、副作用を持つ処理と純粋関数の境界線を明確に引くことで、テスト可能な領域を切り出すことができます。
そして、純粋関数として抽出されたビジネスロジックは、Rubberduck VBAなどのテストフレームワークを用いて、自動化されたテストの対象となります。

改善ステップ 実施内容 期待される効果
アンチパターンの排除 巨大なSubプロシージャの分割と責務の分離 コードの可読性向上と影響範囲の局所化
依存関係の整理 インターフェース化と依存性の注入(DI)の導入 外部リソースからの疎結合化とモックテストの実現
テストの自動化 純粋関数に対する単体テストの実装 回帰バグの防止と安全なリファクタリング環境の構築

さらに重要なのは、依存性の注入(DI)の考え方を導入することです。
VBAにはDIコンテナが存在しませんが、クラスモジュールとImplementsキーワードを活用することで、具象クラスではなくインターフェースに依存する設計を実現できます。
これにより、テスト時には外部リソースにアクセスしないテストダブル(モック)を注入できるようになり、単体テストの実行速度と確定性が劇的に向上します。

エントリーポイントでの依存関係の組み立ては、以下のように行います。
これにより、本番環境とテスト環境で異なる依存関係を同じビジネスロジックに注入できるようになります。

' エントリーポイントでの依存関係の組み立て例
Sub Main()
    ' 本番環境用の依存関係を構築
    Dim dbService As IDbRepository
    Set dbService = New SqlDbRepository

    Dim calculator As New OrderCalculator
    calculator.Init dbService

    ' 処理の実行
    calculator.ExecuteCalculation
End Sub

このような設計は、単にテストを書きやすくするだけでなく、システム全体の柔軟性を高めます。
例えば、将来的にデータベースがSQL ServerからクラウドのWeb APIへ移行された場合でも、インターフェースを実装した新しい具象クラスを作成するだけで対応可能となり、ビジネスロジック側のコードは一切変更する必要がありません。
これは、ソフトウェア工学における開放閉鎖の原則(OCP)の実践でもあります。

レガシーコードの近代化は、一度の巨視的な書き換えで達成されるものではありません。
特徴付けテストによる安全網の構築と、小さなステップでのリファクタリングの積み重ねが不可欠です。
そして最終的には、ソースコードの外部ファイル化とCIパイプラインへの組み込みにより、継続的インテグレーションの環境を整えることが、長期的な品質担保の鍵となります。

プログラミング言語や実行環境がどれほど古くても、ソフトウェア設計の普遍的な原則は普遍的な価値を持ちます。
VBAだからといって設計を疎かにするのではなく、論理的な分析と適切なリファクタリング手法を適用することで、誰もが保守しやすく、堅牢なシステムを構築することができるのです。
本記事の知見を実践に移し、レガシーなVBAコードを自信を持って保守できる近代的な資産へと進化させてください。

コメント

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