【VBA】VBAコードの動作テストを自動化する方法(コピペOK)

VBA
スポンサーリンク

Contents

スポンサーリンク
  1. VBAのコード、「動いてるから大丈夫」で済ませていませんか?
  2. どんな場面で使う?
  3. Before / After:テスト自動化で何が変わるか
    1. Before(手動テスト)
    2. After(自動テスト導入後)
  4. ストーリー:テスト自動化が救った現場
  5. 事前準備
    1. 必要な環境
    2. モジュール構成
    3. モジュールの追加手順
  6. 手順の全体像
  7. 最小限コード:シンプルなテストランナー
    1. Step 1:テスト対象の関数
    2. Step 2:Assert関数(最小版)
    3. Step 3:テストケースを書く
    4. Step 4:テストランナー(最小版)
  8. 最小限コードの解説
    1. Assert関数の仕組み
    2. テストケースの命名規則
    3. テストランナーの役割
  9. 実務版コード:本格テストスイートフレームワーク
    1. MAssert モジュール(実務版)
    2. MTestRunner モジュール(実務版)
    3. MTests モジュール(実務版)
  10. 実務版コードの解説
    1. テストの自動発見の仕組み
    2. 処理時間の計測
    3. シートへの結果出力
    4. エラーハンドリングの工夫
    5. AssertError の使い方
  11. よくある落とし穴と対処法
    1. 落とし穴1:VBProjectへのアクセスが拒否される
    2. 落とし穴2:テスト内でシートを操作するとテスト同士が干渉する
    3. 落とし穴3:浮動小数点の比較で失敗する
    4. 落とし穴4:Application.Runで日本語プロシージャ名が動かない
    5. 落とし穴5:テスト結果シートが保護されている
    6. 落とし穴6:テスト対象のコードがPrivateで参照できない
    7. 落とし穴7:大量テストの実行でメモリ不足になる
    8. VBAのテスト自動実行でVBProjectアクセスが拒否されるときの対処法
    9. VBAのテストが一括実行で失敗するときの対処法
  12. よくある質問(FAQ)
    1. Q1:テストはいつ実行すればよいですか?
    2. Q2:テストケースはどのくらい書けばよいですか?
    3. Q3:テスト対象の関数がシートのデータに依存しています。どうすればよいですか?
    4. Q4:MsgBoxやInputBoxが含まれるコードはテストできますか?
    5. Q5:テストを特定のモジュールだけ実行することはできますか?
    6. Q6:テストが失敗したとき、デバッグのコツはありますか?
    7. Q7:他のブックの関数もテストできますか?
  13. まとめ
    1. この記事で学んだこと
    2. テスト自動化を成功させるポイント
  14. 次のステップ

VBAのコード、「動いてるから大丈夫」で済ませていませんか?

「先月まで正常に動いていたマクロが、今月になって突然おかしくなった」

「一箇所修正したら、別の場所が壊れていた」

「修正するたびに手動で全パターン確認するのが本当に面倒…」

VBAで業務マクロを作っていると、こうした問題に必ずぶつかります。特にコードが大きくなってくると、ちょっとした修正が思わぬバグを生むことがあります。いわゆる「デグレード(回帰バグ)」です。

プロの開発現場では、こうした問題を防ぐために自動テストを導入しています。コードを修正するたびに、ボタンひとつで全機能の動作チェックを実行する仕組みです。

「でも、自動テストってVBAでもできるの?」

はい、できます。しかも、意外とシンプルな仕組みで実現できます。

この記事では、VBAで使えるAssert関数による単体テストの基本から、複数のテストを一括実行するテストスイート結果をシートにレポート出力する実務版まで、すべてコピペで使えるコード付きで解説します。

どんな場面で使う?

  • マクロを修正するたびに手動で全パターン確認する手間をなくしたいとき
  • コードの一部を修正したら別の場所が壊れる「回帰バグ」を自動で検出したいとき
  • 関数単位のテストを書いて、ロジックの正しさを保証しながら開発したいとき
  • テスト結果をシートに一覧出力して、PASS/FAILを可視化したいとき

Before / After:テスト自動化で何が変わるか

Before(手動テスト)

項目 状態
テスト方法 手動でExcelを操作して目視確認
所要時間 修正のたびに15〜30分
テスト漏れ 確認し忘れるパターンが必ず出る
回帰テスト 面倒なのでやらない(やれない)
品質の信頼度 「たぶん大丈夫」レベル

After(自動テスト導入後)

項目 状態
テスト方法 マクロ実行で全パターン自動チェック
所要時間 ボタンひとつで数秒〜数十秒
テスト漏れ テストケースとして記録されるので漏れない
回帰テスト 毎回全テスト実行するので安心
品質の信頼度 「テスト全件パスしている」と言い切れる

ストーリー:テスト自動化が救った現場

経理部門の田中さんは、毎月の決算処理を行うVBAマクロを管理していました。50以上のプロシージャから成る大規模なマクロです。

ある月、消費税の計算ロジックにバグが見つかり、修正を行いました。修正自体は1行の変更でしたが、翌月の決算で別の帳票の金額がずれていることが発覚。原因を調べると、修正した関数を別の帳票でも使っていたのです。

「修正するたびに全帳票を手動でチェックするのは無理だ…」

そこで田中さんは、主要な計算関数に対して自動テストを導入しました。各関数の入力と期待結果をテストコードとして記録し、修正のたびにワンクリックで全テストを実行する仕組みです。

導入後、別の修正で回帰バグが発生しかけましたが、テストが即座に検出。手動確認では見逃していたであろうバグを未然に防ぐことができました。

田中さんは言います。「テストコードを書く時間は、バグ対応に費やす時間に比べたら微々たるもの。もっと早く導入すればよかった」

事前準備

必要な環境

  • Excel 2010以降(Windows版)
  • VBAエディタ(Alt + F11で起動)

モジュール構成

この記事では、以下の3つの標準モジュールを作成します。

モジュール名 役割
MAssert Assert関数群(テストの判定ロジック)
MTestRunner テスト実行エンジン(テストの発見・実行・レポート)
MTests テストケース(実際のテストコード)

モジュールの追加手順

  1. Alt + F11でVBAエディタを開く
  2. 左側のプロジェクトエクスプローラーで対象ブックを右クリック
  3. 「挿入」→「標準モジュール」を選択
  4. プロパティウィンドウで(オブジェクト名)を上記の名前に変更
  5. 3つのモジュール分、この手順を繰り返す

VBAエディタの基本操作やデバッグの方法については、VBAのデバッグ技術を使いこなす方法で詳しく解説しています。

手順の全体像

自動テストの導入は、以下の5ステップで進めます。

ステップ 内容
Step 1 テスト対象の関数を用意する
Step 2 Assert関数モジュールを作成する
Step 3 テストケースを書く
Step 4 テストランナーで実行する
Step 5 結果を確認する

最小限コード:シンプルなテストランナー

まずは最小限のコードで、テスト自動化の全体像を掴みましょう。

Step 1:テスト対象の関数

税込価格を計算する簡単な関数をテスト対象とします。


'================================================
' テスト対象の関数(本番コードに相当)
'================================================
Public Function CalcTax(ByVal price As Long, ByVal taxRate As Double) As Long
    '----------------------------------------------
    ' 機能  : 税込価格を計算する
    ' 引数  : price   - 税抜価格
    '        taxRate - 税率(例:0.1 = 10%)
    ' 戻り値 : 税込価格(小数点以下切り捨て)
    '----------------------------------------------
    CalcTax = Int(price * (1 + taxRate))
End Function

Public Function CalcDiscount(ByVal price As Long, ByVal discountPercent As Long) As Long
    '----------------------------------------------
    ' 機能  : 割引後の価格を計算する
    ' 引数  : price           - 元の価格
    '        discountPercent - 割引率(%)
    ' 戻り値 : 割引後の価格(小数点以下切り捨て)
    '----------------------------------------------
    If discountPercent < 0 Or discountPercent > 100 Then
        CalcDiscount = -1  ' エラーを示す
        Exit Function
    End If
    CalcDiscount = Int(price * (100 - discountPercent) / 100)
End Function

Step 2:Assert関数(最小版)

テストの合否を判定するAssert関数を作ります。


'================================================
' MAssert モジュール(最小版)
' テストの合否判定を行うAssert関数群
'================================================
Option Explicit

' テスト結果を格納する変数
Private mPassCount As Long
Private mFailCount As Long
Private mResults As String

Public Sub InitTest()
    '----------------------------------------------
    ' テスト結果を初期化する
    '----------------------------------------------
    mPassCount = 0
    mFailCount = 0
    mResults = ""
End Sub

Public Sub AssertEqual(ByVal testName As String, _
                       ByVal expected As Variant, _
                       ByVal actual As Variant)
    '----------------------------------------------
    ' 期待値と実際の値が一致するか判定する
    ' 引数  : testName - テスト名(結果表示用)
    '        expected - 期待値
    '        actual   - 実際の値
    '----------------------------------------------
    If expected = actual Then
        mPassCount = mPassCount + 1
        mResults = mResults & "[PASS] " & testName & vbCrLf
    Else
        mFailCount = mFailCount + 1
        mResults = mResults & "[FAIL] " & testName & _
                   " (期待値: " & CStr(expected) & _
                   "  実際: " & CStr(actual) & ")" & vbCrLf
    End If
End Sub

Public Sub AssertTrue(ByVal testName As String, _
                      ByVal condition As Boolean)
    '----------------------------------------------
    ' 条件がTrueであるか判定する
    ' 引数  : testName  - テスト名
    '        condition - 判定する条件式
    '----------------------------------------------
    If condition Then
        mPassCount = mPassCount + 1
        mResults = mResults & "[PASS] " & testName & vbCrLf
    Else
        mFailCount = mFailCount + 1
        mResults = mResults & "[FAIL] " & testName & _
                   " (Trueを期待したがFalseだった)" & vbCrLf
    End If
End Sub

Public Sub ShowResults()
    '----------------------------------------------
    ' テスト結果をメッセージボックスに表示する
    '----------------------------------------------
    Dim summary As String
    summary = "=== テスト結果 ===" & vbCrLf
    summary = summary & "合格: " & mPassCount & " 件" & vbCrLf
    summary = summary & "失敗: " & mFailCount & " 件" & vbCrLf
    summary = summary & "合計: " & (mPassCount + mFailCount) & " 件" & vbCrLf
    summary = summary & vbCrLf & mResults

    MsgBox summary, IIf(mFailCount = 0, vbInformation, vbExclamation), _
           "テスト結果"
End Sub

Public Property Get PassCount() As Long
    PassCount = mPassCount
End Property

Public Property Get FailCount() As Long
    FailCount = mFailCount
End Property

Public Property Get ResultText() As String
    ResultText = mResults
End Property

Step 3:テストケースを書く


'================================================
' MTests モジュール(最小版)
' テスト対象の関数に対するテストケース
'================================================
Option Explicit

Public Sub Test_CalcTax_通常の税率10パーセント()
    '----------------------------------------------
    ' CalcTax関数のテスト:標準税率(10%)
    '----------------------------------------------
    AssertEqual "税込計算_1000円_10%", 1100, CalcTax(1000, 0.1)
    AssertEqual "税込計算_500円_10%", 550, CalcTax(500, 0.1)
    AssertEqual "税込計算_0円_10%", 0, CalcTax(0, 0.1)
End Sub

Public Sub Test_CalcTax_軽減税率8パーセント()
    '----------------------------------------------
    ' CalcTax関数のテスト:軽減税率(8%)
    '----------------------------------------------
    AssertEqual "税込計算_1000円_8%", 1080, CalcTax(1000, 0.08)
    AssertEqual "税込計算_999円_8%", 1078, CalcTax(999, 0.08)
End Sub

Public Sub Test_CalcDiscount_正常系()
    '----------------------------------------------
    ' CalcDiscount関数のテスト:正常な割引率
    '----------------------------------------------
    AssertEqual "割引_1000円_10%OFF", 900, CalcDiscount(1000, 10)
    AssertEqual "割引_1000円_50%OFF", 500, CalcDiscount(1000, 50)
    AssertEqual "割引_1000円_0%OFF", 1000, CalcDiscount(1000, 0)
    AssertEqual "割引_1000円_100%OFF", 0, CalcDiscount(1000, 100)
End Sub

Public Sub Test_CalcDiscount_異常系()
    '----------------------------------------------
    ' CalcDiscount関数のテスト:不正な割引率
    '----------------------------------------------
    AssertEqual "割引_マイナス割引率", -1, CalcDiscount(1000, -10)
    AssertEqual "割引_100超の割引率", -1, CalcDiscount(1000, 150)
End Sub

Step 4:テストランナー(最小版)


'================================================
' テスト実行プロシージャ(最小版)
' すべてのテストを順番に実行する
'================================================
Public Sub RunAllTests()
    '----------------------------------------------
    ' すべてのテストケースを実行して結果を表示する
    '----------------------------------------------

    ' テスト結果を初期化
    InitTest

    ' 各テストを実行
    Test_CalcTax_通常の税率10パーセント
    Test_CalcTax_軽減税率8パーセント
    Test_CalcDiscount_正常系
    Test_CalcDiscount_異常系

    ' 結果を表示
    ShowResults

End Sub

実行すると、メッセージボックスに以下のような結果が表示されます。


=== テスト結果 ===
合格: 9 件
失敗: 0 件
合計: 9 件

[PASS] 税込計算_1000円_10%
[PASS] 税込計算_500円_10%
[PASS] 税込計算_0円_10%
[PASS] 税込計算_1000円_8%
[PASS] 税込計算_999円_8%
[PASS] 割引_1000円_10%OFF
[PASS] 割引_1000円_50%OFF
[PASS] 割引_1000円_0%OFF
[PASS] 割引_1000円_100%OFF

最小限コードの解説

Assert関数の仕組み

Assert関数は「期待する結果」と「実際の結果」を比較して、一致すればPASS、不一致ならFAILと記録するシンプルな仕組みです。


AssertEqual "テスト名", 期待値, 実際の値

この1行で、以下の処理が行われます。

  1. 期待値実際の値を比較
  2. 一致すればmPassCountを加算し、結果に[PASS]を記録
  3. 不一致ならmFailCountを加算し、結果に[FAIL]と差分を記録

テストケースの命名規則

テストプロシージャの名前はTest_で始める規約にしています。これは後の実務版で、テストの自動発見機能に活用します。


Public Sub Test_CalcTax_通常の税率10パーセント()
'           ^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^
'           対象関数  テスト内容の説明

Test_ + 対象の関数名 + _ + テストの内容 という命名にすると、テスト結果を見たときに何をテストしているかが一目でわかります。

テストランナーの役割

テストランナーは、すべてのテストを順番に呼び出す「指揮者」のような存在です。最小版では単純に各テストプロシージャを手動で列挙していますが、実務版ではこれを自動化します。

変数の中身を確認するデバッグ手法については、Debug.Printとイミディエイトウィンドウで変数の中身を確認する方法も参考にしてください。

実務版コード:本格テストスイートフレームワーク

最小版では手動でテスト関数を呼び出していましたが、実務版では以下の機能を追加します。

  • テストプロシージャの自動発見Test_で始まるプロシージャを自動検出)
  • テスト結果のシート出力(専用シートに一覧表示)
  • 処理時間の計測(各テスト・全体の実行時間を記録)
  • AssertNotEqual / AssertFalse / AssertErrorの追加
  • テストのスキップ機能
  • カラー表示(PASS=緑、FAIL=赤、SKIP=黄色)

MAssert モジュール(実務版)


'================================================
' MAssert モジュール(実務版)
' テストの合否判定を行うAssert関数群
'
' 機能:
'   - AssertEqual    : 値の一致を検証
'   - AssertNotEqual : 値の不一致を検証
'   - AssertTrue     : 条件がTrueであることを検証
'   - AssertFalse    : 条件がFalseであることを検証
'   - AssertError    : エラーが発生することを検証
'   - SkipTest       : テストをスキップ扱いにする
'
' 使い方:
'   1. InitTest でテスト結果を初期化
'   2. 各Assert関数でテストを実行
'   3. GetResults で結果配列を取得
'================================================
Option Explicit

' --- テスト結果の定数 ---
Public Const TEST_PASS As String = "PASS"
Public Const TEST_FAIL As String = "FAIL"
Public Const TEST_SKIP As String = "SKIP"
Public Const TEST_ERROR As String = "ERROR"

' --- テスト結果の格納用 ---
Private Type TestResult
    TestName As String       ' テスト名
    Status As String         ' PASS / FAIL / SKIP / ERROR
    Expected As String       ' 期待値(文字列表現)
    Actual As String         ' 実際の値(文字列表現)
    Message As String        ' 補足メッセージ
    ModuleName As String     ' テストが属するモジュール名
    ElapsedMs As Double      ' 個別テストの実行時間(ms)
End Type

Private mResults() As TestResult
Private mResultCount As Long
Private mPassCount As Long
Private mFailCount As Long
Private mSkipCount As Long
Private mErrorCount As Long
Private mCurrentModule As String
Private mTestStartTime As Double

' --- Win32 API:高精度タイマー ---
#If VBA7 Then
    Private Declare PtrSafe Function QueryPerformanceCounter _
        Lib "kernel32" (lpPerformanceCount As Currency) As Long
    Private Declare PtrSafe Function QueryPerformanceFrequency _
        Lib "kernel32" (lpFrequency As Currency) As Long
#Else
    Private Declare Function QueryPerformanceCounter _
        Lib "kernel32" (lpPerformanceCount As Currency) As Long
    Private Declare Function QueryPerformanceFrequency _
        Lib "kernel32" (lpFrequency As Currency) As Long
#End If

Private mFrequency As Currency

Public Sub InitTest()
    '----------------------------------------------
    ' テスト結果を初期化する
    ' すべてのテスト実行前に必ず呼ぶこと
    '----------------------------------------------
    mResultCount = 0
    mPassCount = 0
    mFailCount = 0
    mSkipCount = 0
    mErrorCount = 0
    mCurrentModule = ""
    Erase mResults
    QueryPerformanceFrequency mFrequency
End Sub

Public Sub SetCurrentModule(ByVal moduleName As String)
    '----------------------------------------------
    ' 現在のテストモジュール名を設定する
    ' 引数: moduleName - モジュール名
    '----------------------------------------------
    mCurrentModule = moduleName
End Sub

Public Sub StartTimer()
    '----------------------------------------------
    ' 個別テストのタイマーを開始する
    '----------------------------------------------
    QueryPerformanceCounter mTestStartTime
End Sub

Private Function GetElapsedMs() As Double
    '----------------------------------------------
    ' タイマー開始からの経過時間をミリ秒で返す
    '----------------------------------------------
    Dim endTime As Currency
    QueryPerformanceCounter endTime
    If mFrequency > 0 Then
        GetElapsedMs = (endTime - mTestStartTime) / mFrequency * 1000
    Else
        GetElapsedMs = 0
    End If
End Function

Private Sub AddResult(ByVal testName As String, _
                      ByVal status As String, _
                      ByVal expected As String, _
                      ByVal actual As String, _
                      Optional ByVal message As String = "")
    '----------------------------------------------
    ' テスト結果を内部配列に追加する
    '----------------------------------------------
    mResultCount = mResultCount + 1
    ReDim Preserve mResults(1 To mResultCount)

    With mResults(mResultCount)
        .TestName = testName
        .Status = status
        .Expected = expected
        .Actual = actual
        .Message = message
        .ModuleName = mCurrentModule
        .ElapsedMs = GetElapsedMs()
    End With
End Sub

Public Sub AssertEqual(ByVal testName As String, _
                       ByVal expected As Variant, _
                       ByVal actual As Variant)
    '----------------------------------------------
    ' 期待値と実際の値が一致するか判定する
    ' 引数: testName - テスト名
    '       expected - 期待値
    '       actual   - 実際の値
    '----------------------------------------------
    If expected = actual Then
        mPassCount = mPassCount + 1
        AddResult testName, TEST_PASS, CStr(expected), CStr(actual)
    Else
        mFailCount = mFailCount + 1
        AddResult testName, TEST_FAIL, CStr(expected), CStr(actual), _
                  "値が一致しません"
    End If
End Sub

Public Sub AssertNotEqual(ByVal testName As String, _
                          ByVal notExpected As Variant, _
                          ByVal actual As Variant)
    '----------------------------------------------
    ' 値が不一致であることを判定する
    ' 引数: testName    - テスト名
    '       notExpected - この値と一致しないことを期待
    '       actual      - 実際の値
    '----------------------------------------------
    If notExpected <> actual Then
        mPassCount = mPassCount + 1
        AddResult testName, TEST_PASS, "<>" & CStr(notExpected), CStr(actual)
    Else
        mFailCount = mFailCount + 1
        AddResult testName, TEST_FAIL, "<>" & CStr(notExpected), CStr(actual), _
                  "値が一致してしまっています"
    End If
End Sub

Public Sub AssertTrue(ByVal testName As String, _
                      ByVal condition As Boolean)
    '----------------------------------------------
    ' 条件がTrueであるか判定する
    ' 引数: testName  - テスト名
    '       condition - 判定する条件式
    '----------------------------------------------
    If condition Then
        mPassCount = mPassCount + 1
        AddResult testName, TEST_PASS, "True", "True"
    Else
        mFailCount = mFailCount + 1
        AddResult testName, TEST_FAIL, "True", "False", _
                  "Trueを期待しましたがFalseでした"
    End If
End Sub

Public Sub AssertFalse(ByVal testName As String, _
                       ByVal condition As Boolean)
    '----------------------------------------------
    ' 条件がFalseであるか判定する
    ' 引数: testName  - テスト名
    '       condition - 判定する条件式
    '----------------------------------------------
    If Not condition Then
        mPassCount = mPassCount + 1
        AddResult testName, TEST_PASS, "False", "False"
    Else
        mFailCount = mFailCount + 1
        AddResult testName, TEST_FAIL, "False", "True", _
                  "Falseを期待しましたがTrueでした"
    End If
End Sub

Public Sub AssertError(ByVal testName As String, _
                       ByVal errNumber As Long)
    '----------------------------------------------
    ' 直前にOn Error Resume Nextで捕捉したエラーを検証する
    ' 使用例:
    '   On Error Resume Next
    '   result = SomeFunction()  ' エラーが起きるはず
    '   AssertError "エラーテスト", 11  ' 0除算エラー
    '   On Error GoTo 0
    '
    ' 引数: testName  - テスト名
    '       errNumber - 期待するエラー番号
    '----------------------------------------------
    If Err.Number = errNumber Then
        mPassCount = mPassCount + 1
        AddResult testName, TEST_PASS, "Error " & errNumber, _
                  "Error " & Err.Number
    Else
        mFailCount = mFailCount + 1
        AddResult testName, TEST_FAIL, "Error " & errNumber, _
                  "Error " & Err.Number, _
                  "期待したエラーが発生しませんでした"
    End If
    Err.Clear
End Sub

Public Sub SkipTest(ByVal testName As String, _
                    Optional ByVal reason As String = "")
    '----------------------------------------------
    ' テストをスキップ扱いにする
    ' 引数: testName - テスト名
    '       reason   - スキップ理由
    '----------------------------------------------
    mSkipCount = mSkipCount + 1
    AddResult testName, TEST_SKIP, "", "", reason
End Sub

Public Sub RecordError(ByVal testName As String, _
                       ByVal errMsg As String)
    '----------------------------------------------
    ' テスト実行中のエラーを記録する
    ' 引数: testName - テスト名
    '       errMsg   - エラーメッセージ
    '----------------------------------------------
    mErrorCount = mErrorCount + 1
    AddResult testName, TEST_ERROR, "", "", errMsg
End Sub

' === プロパティ ===

Public Property Get PassCount() As Long
    PassCount = mPassCount
End Property

Public Property Get FailCount() As Long
    FailCount = mFailCount
End Property

Public Property Get SkipCount() As Long
    SkipCount = mSkipCount
End Property

Public Property Get ErrorCount() As Long
    ErrorCount = mErrorCount
End Property

Public Property Get TotalCount() As Long
    TotalCount = mPassCount + mFailCount + mSkipCount + mErrorCount
End Property

Public Property Get ResultCount() As Long
    ResultCount = mResultCount
End Property

Public Function GetResultItem(ByVal index As Long) As Variant
    '----------------------------------------------
    ' 指定インデックスのテスト結果を配列で返す
    ' 戻り値: Array(テスト名, ステータス, 期待値,
    '               実際の値, メッセージ, モジュール名, 実行時間ms)
    '----------------------------------------------
    If index < 1 Or index > mResultCount Then
        GetResultItem = Array("", "", "", "", "", "", 0)
        Exit Function
    End If

    With mResults(index)
        GetResultItem = Array(.TestName, .Status, .Expected, _
                              .Actual, .Message, .ModuleName, .ElapsedMs)
    End With
End Function

MTestRunner モジュール(実務版)


'================================================
' MTestRunner モジュール(実務版)
' テストの自動発見・実行・レポート出力
'
' 機能:
'   - Test_ で始まるプロシージャを自動発見して実行
'   - テスト結果を専用シートに出力
'   - 全体の実行時間を計測
'   - PASS/FAIL/SKIPをカラー表示
'
' 使い方:
'   RunAllTests を実行するだけ
'================================================
Option Explicit

' --- Win32 API:高精度タイマー ---
#If VBA7 Then
    Private Declare PtrSafe Function QPC _
        Lib "kernel32" Alias "QueryPerformanceCounter" _
        (lpPerformanceCount As Currency) As Long
    Private Declare PtrSafe Function QPF _
        Lib "kernel32" Alias "QueryPerformanceFrequency" _
        (lpFrequency As Currency) As Long
#Else
    Private Declare Function QPC _
        Lib "kernel32" Alias "QueryPerformanceCounter" _
        (lpPerformanceCount As Currency) As Long
    Private Declare Function QPF _
        Lib "kernel32" Alias "QueryPerformanceFrequency" _
        (lpFrequency As Currency) As Long
#End If

' --- 定数 ---
Private Const RESULT_SHEET_NAME As String = "TestResults"
Private Const TEST_PREFIX As String = "Test_"

' --- カラー定数 ---
Private Const COLOR_PASS As Long = 5287936    ' 緑 RGB(0,176,80)
Private Const COLOR_FAIL As Long = 255        ' 赤 RGB(255,0,0)
Private Const COLOR_SKIP As Long = 49407      ' 黄 RGB(255,192,0)
Private Const COLOR_ERROR As Long = 10053120  ' 紫 RGB(128,0,153)
Private Const COLOR_HEADER As Long = 12566463 ' ライトグレー RGB(191,191,191)
Private Const COLOR_WHITE As Long = 16777215  ' 白

Public Sub RunAllTests()
    '----------------------------------------------
    ' メインのテスト実行プロシージャ
    ' Test_ で始まるすべてのプロシージャを検出・実行し
    ' 結果を専用シートに出力する
    '----------------------------------------------
    Dim startTime As Currency
    Dim endTime As Currency
    Dim freq As Currency
    Dim totalElapsed As Double

    ' 画面更新を停止(高速化)
    Application.ScreenUpdating = False
    Application.Calculation = xlCalculationManual

    ' タイマー開始
    QPF freq
    QPC startTime

    ' テスト結果を初期化
    InitTest

    ' テストプロシージャを収集
    Dim testProcs() As String
    Dim testModules() As String
    Dim testCount As Long
    testCount = CollectTestProcedures(testProcs, testModules)

    If testCount = 0 Then
        MsgBox "Test_ で始まるプロシージャが見つかりません。" & vbCrLf & _
               "テストモジュールにTest_で始まるSubプロシージャを作成してください。", _
               vbExclamation, "テスト未検出"
        GoTo Cleanup
    End If

    ' 各テストを実行
    Dim i As Long
    For i = 1 To testCount
        ExecuteOneTest testProcs(i), testModules(i)
    Next i

    ' タイマー終了
    QPC endTime
    If freq > 0 Then
        totalElapsed = (endTime - startTime) / freq * 1000
    End If

    ' 結果をシートに出力
    OutputResultsToSheet totalElapsed

    ' サマリーメッセージ
    Dim msg As String
    msg = "テスト完了!" & vbCrLf & vbCrLf
    msg = msg & "合格: " & PassCount & " 件" & vbCrLf
    msg = msg & "失敗: " & FailCount & " 件" & vbCrLf
    msg = msg & "スキップ: " & SkipCount & " 件" & vbCrLf
    msg = msg & "エラー: " & ErrorCount & " 件" & vbCrLf
    msg = msg & "合計: " & TotalCount & " 件" & vbCrLf
    msg = msg & vbCrLf & "実行時間: " & Format(totalElapsed, "#,##0.0") & " ms"

    Dim icon As VbMsgBoxStyle
    If FailCount = 0 And ErrorCount = 0 Then
        icon = vbInformation
    Else
        icon = vbExclamation
    End If

    MsgBox msg, icon, "テスト結果サマリー"

Cleanup:
    Application.ScreenUpdating = True
    Application.Calculation = xlCalculationAutomatic
End Sub

Private Function CollectTestProcedures(ByRef procNames() As String, _
                                        ByRef modNames() As String) As Long
    '----------------------------------------------
    ' VBProject内からTest_で始まるSubプロシージャを収集
    ' 戻り値: 見つかったテスト数
    '----------------------------------------------
    Dim vbProj As Object
    Dim vbComp As Object
    Dim codeMod As Object
    Dim lineNum As Long
    Dim procName As String
    Dim procKind As Long  ' vbext_pk_Proc = 0
    Dim count As Long

    Set vbProj = ThisWorkbook.VBProject

    ' まずテスト数をカウント
    count = 0
    For Each vbComp In vbProj.VBComponents
        ' 標準モジュールのみ対象(Type = 1)
        If vbComp.Type = 1 Then
            Set codeMod = vbComp.CodeModule
            lineNum = 1
            Do While lineNum < codeMod.CountOfLines
                procName = codeMod.ProcOfLine(lineNum, procKind)
                If Len(procName) > 0 Then
                    If Left(procName, Len(TEST_PREFIX)) = TEST_PREFIX Then
                        count = count + 1
                    End If
                    ' 次のプロシージャへスキップ
                    lineNum = lineNum + _
                              codeMod.ProcCountLines(procName, procKind)
                Else
                    lineNum = lineNum + 1
                End If
            Loop
        End If
    Next vbComp

    If count = 0 Then
        CollectTestProcedures = 0
        Exit Function
    End If

    ' 配列に格納
    ReDim procNames(1 To count)
    ReDim modNames(1 To count)

    Dim idx As Long
    idx = 0
    Dim prevProc As String

    For Each vbComp In vbProj.VBComponents
        If vbComp.Type = 1 Then
            Set codeMod = vbComp.CodeModule
            lineNum = 1
            prevProc = ""
            Do While lineNum < codeMod.CountOfLines
                procName = codeMod.ProcOfLine(lineNum, procKind)
                If Len(procName) > 0 Then
                    If Left(procName, Len(TEST_PREFIX)) = TEST_PREFIX Then
                        ' 同じプロシージャを重複追加しない
                        If procName <> prevProc Then
                            idx = idx + 1
                            procNames(idx) = procName
                            modNames(idx) = vbComp.Name
                            prevProc = procName
                        End If
                    End If
                    lineNum = lineNum + _
                              codeMod.ProcCountLines(procName, procKind)
                Else
                    lineNum = lineNum + 1
                End If
            Loop
        End If
    Next vbComp

    CollectTestProcedures = idx
End Function

Private Sub ExecuteOneTest(ByVal procName As String, _
                           ByVal moduleName As String)
    '----------------------------------------------
    ' テストプロシージャを1つ実行する
    ' エラーが発生しても他のテストに影響しないよう
    ' エラーハンドリングで保護する
    '----------------------------------------------
    SetCurrentModule moduleName
    StartTimer

    On Error GoTo ErrHandler
    Application.Run procName
    On Error GoTo 0
    Exit Sub

ErrHandler:
    RecordError procName, _
                "実行時エラー " & Err.Number & ": " & Err.Description
    On Error GoTo 0
End Sub

Private Sub OutputResultsToSheet(ByVal totalMs As Double)
    '----------------------------------------------
    ' テスト結果を専用シートに出力する
    ' シートが存在しなければ新規作成する
    '----------------------------------------------
    Dim ws As Worksheet

    ' 結果シートを取得または作成
    Set ws = GetOrCreateSheet(RESULT_SHEET_NAME)
    ws.Cells.Clear

    ' --- ヘッダー行 ---
    Dim headers As Variant
    headers = Array("No.", "モジュール", "テスト名", "結果", _
                    "期待値", "実際の値", "メッセージ", "実行時間(ms)")

    Dim col As Long
    For col = 1 To 8
        ws.Cells(1, col).Value = headers(col - 1)
    Next col

    ' ヘッダーの書式設定
    With ws.Range("A1:H1")
        .Font.Bold = True
        .Interior.Color = COLOR_HEADER
        .Borders(xlEdgeBottom).LineStyle = xlContinuous
        .Borders(xlEdgeBottom).Weight = xlMedium
    End With

    ' --- データ行 ---
    Dim row As Long
    Dim resultItem As Variant

    For row = 1 To ResultCount
        resultItem = GetResultItem(row)

        ws.Cells(row + 1, 1).Value = row                  ' No.
        ws.Cells(row + 1, 2).Value = resultItem(5)         ' モジュール
        ws.Cells(row + 1, 3).Value = resultItem(0)         ' テスト名
        ws.Cells(row + 1, 4).Value = resultItem(1)         ' 結果
        ws.Cells(row + 1, 5).Value = resultItem(2)         ' 期待値
        ws.Cells(row + 1, 6).Value = resultItem(3)         ' 実際の値
        ws.Cells(row + 1, 7).Value = resultItem(4)         ' メッセージ
        ws.Cells(row + 1, 8).Value = Format(resultItem(6), "0.000") ' 実行時間

        ' 結果に応じた色分け
        Select Case CStr(resultItem(1))
            Case TEST_PASS
                ws.Cells(row + 1, 4).Font.Color = COLOR_PASS
            Case TEST_FAIL
                ws.Cells(row + 1, 4).Font.Color = COLOR_FAIL
                ws.Cells(row + 1, 4).Font.Bold = True
                ' FAIL行全体を薄い赤で強調
                ws.Range(ws.Cells(row + 1, 1), ws.Cells(row + 1, 8)) _
                    .Interior.Color = RGB(255, 230, 230)
            Case TEST_SKIP
                ws.Cells(row + 1, 4).Font.Color = COLOR_SKIP
            Case TEST_ERROR
                ws.Cells(row + 1, 4).Font.Color = COLOR_ERROR
                ws.Cells(row + 1, 4).Font.Bold = True
                ws.Range(ws.Cells(row + 1, 1), ws.Cells(row + 1, 8)) _
                    .Interior.Color = RGB(240, 220, 255)
        End Select
    Next row

    ' --- サマリー行 ---
    Dim summaryRow As Long
    summaryRow = ResultCount + 3

    ws.Cells(summaryRow, 1).Value = "=== テスト結果サマリー ==="
    ws.Cells(summaryRow, 1).Font.Bold = True

    ws.Cells(summaryRow + 1, 1).Value = "合格"
    ws.Cells(summaryRow + 1, 2).Value = PassCount
    ws.Cells(summaryRow + 1, 2).Font.Color = COLOR_PASS

    ws.Cells(summaryRow + 2, 1).Value = "失敗"
    ws.Cells(summaryRow + 2, 2).Value = FailCount
    ws.Cells(summaryRow + 2, 2).Font.Color = COLOR_FAIL

    ws.Cells(summaryRow + 3, 1).Value = "スキップ"
    ws.Cells(summaryRow + 3, 2).Value = SkipCount
    ws.Cells(summaryRow + 3, 2).Font.Color = COLOR_SKIP

    ws.Cells(summaryRow + 4, 1).Value = "エラー"
    ws.Cells(summaryRow + 4, 2).Value = ErrorCount
    ws.Cells(summaryRow + 4, 2).Font.Color = COLOR_ERROR

    ws.Cells(summaryRow + 5, 1).Value = "合計"
    ws.Cells(summaryRow + 5, 2).Value = TotalCount
    ws.Cells(summaryRow + 5, 1).Font.Bold = True
    ws.Cells(summaryRow + 5, 2).Font.Bold = True

    ws.Cells(summaryRow + 7, 1).Value = "実行時間"
    ws.Cells(summaryRow + 7, 2).Value = Format(totalMs, "#,##0.0") & " ms"

    ws.Cells(summaryRow + 8, 1).Value = "実行日時"
    ws.Cells(summaryRow + 8, 2).Value = Format(Now, "yyyy/mm/dd hh:nn:ss")

    ' --- 列幅の自動調整 ---
    ws.Columns("A:H").AutoFit

    ' シートをアクティブにして結果を表示
    ws.Activate
    ws.Range("A1").Select
End Sub

Private Function GetOrCreateSheet(ByVal sheetName As String) As Worksheet
    '----------------------------------------------
    ' 指定名のシートを取得する(なければ作成)
    ' 引数  : sheetName - シート名
    ' 戻り値 : Worksheetオブジェクト
    '----------------------------------------------
    Dim ws As Worksheet

    On Error Resume Next
    Set ws = ThisWorkbook.Worksheets(sheetName)
    On Error GoTo 0

    If ws Is Nothing Then
        Set ws = ThisWorkbook.Worksheets.Add( _
                 After:=ThisWorkbook.Worksheets(ThisWorkbook.Worksheets.count))
        ws.Name = sheetName
    End If

    Set GetOrCreateSheet = ws
End Function

MTests モジュール(実務版)


'================================================
' MTests モジュール(実務版)
' テストケースの実例
'
' 命名規則:
'   Sub Test_[対象関数名]_[テスト内容]()
'   例: Test_CalcTax_標準税率
'================================================
Option Explicit

' ===================================================
'  CalcTax 関数のテスト
' ===================================================

Public Sub Test_CalcTax_標準税率10パーセント()
    '----------------------------------------------
    ' 税率10%の税込計算テスト
    '----------------------------------------------
    AssertEqual "CalcTax(1000, 0.1) = 1100", 1100, CalcTax(1000, 0.1)
    AssertEqual "CalcTax(500, 0.1) = 550", 550, CalcTax(500, 0.1)
    AssertEqual "CalcTax(1, 0.1) = 1", 1, CalcTax(1, 0.1)
End Sub

Public Sub Test_CalcTax_軽減税率8パーセント()
    '----------------------------------------------
    ' 税率8%の税込計算テスト
    '----------------------------------------------
    AssertEqual "CalcTax(1000, 0.08) = 1080", 1080, CalcTax(1000, 0.08)
    AssertEqual "CalcTax(999, 0.08) = 1078", 1078, CalcTax(999, 0.08)
    AssertEqual "CalcTax(123, 0.08) = 132", 132, CalcTax(123, 0.08)
End Sub

Public Sub Test_CalcTax_境界値()
    '----------------------------------------------
    ' 境界値テスト(0円、大きな金額)
    '----------------------------------------------
    AssertEqual "CalcTax(0, 0.1) = 0", 0, CalcTax(0, 0.1)
    AssertEqual "CalcTax(999999, 0.1) = 1099998", _
                1099998, CalcTax(999999, 0.1)
    AssertTrue "CalcTax結果は0以上", CalcTax(100, 0.1) >= 0
End Sub

' ===================================================
'  CalcDiscount 関数のテスト
' ===================================================

Public Sub Test_CalcDiscount_正常な割引()
    '----------------------------------------------
    ' 正常な割引率でのテスト
    '----------------------------------------------
    AssertEqual "10%OFF: 1000 -> 900", 900, CalcDiscount(1000, 10)
    AssertEqual "20%OFF: 1000 -> 800", 800, CalcDiscount(1000, 20)
    AssertEqual "50%OFF: 1000 -> 500", 500, CalcDiscount(1000, 50)
    AssertEqual "33%OFF: 1000 -> 670", 670, CalcDiscount(1000, 33)
End Sub

Public Sub Test_CalcDiscount_境界値()
    '----------------------------------------------
    ' 0%と100%の境界値テスト
    '----------------------------------------------
    AssertEqual "0%OFF: 変化なし", 1000, CalcDiscount(1000, 0)
    AssertEqual "100%OFF: 0円", 0, CalcDiscount(1000, 100)
End Sub

Public Sub Test_CalcDiscount_異常値()
    '----------------------------------------------
    ' 不正な割引率でのエラーハンドリングテスト
    '----------------------------------------------
    AssertEqual "負の割引率はエラー(-1)", -1, CalcDiscount(1000, -10)
    AssertEqual "100超の割引率はエラー(-1)", -1, CalcDiscount(1000, 150)
    AssertNotEqual "正常割引はエラーではない", -1, CalcDiscount(1000, 10)
End Sub

' ===================================================
'  文字列操作関数のテスト(追加例)
' ===================================================

Public Sub Test_文字列結合_基本動作()
    '----------------------------------------------
    ' VBA標準の文字列結合が正しく動くことを確認
    ' (自作関数テストの参考例)
    '----------------------------------------------
    AssertEqual "文字列結合", "Hello World", "Hello" & " " & "World"
    AssertEqual "空文字結合", "ABC", "ABC" & ""
    AssertTrue "Len関数の動作", Len("テスト") = 3
End Sub

Public Sub Test_数値変換_IsNumeric()
    '----------------------------------------------
    ' IsNumericの動作を確認するテスト例
    '----------------------------------------------
    AssertTrue "数値文字列はTrue", IsNumeric("123")
    AssertTrue "小数もTrue", IsNumeric("12.5")
    AssertFalse "文字列はFalse", IsNumeric("ABC")
    AssertFalse "空文字はFalse", IsNumeric("")
End Sub

' ===================================================
'  スキップテストの例
' ===================================================

Public Sub Test_外部連携_API呼び出し()
    '----------------------------------------------
    ' 外部APIへの接続テスト(環境依存のためスキップ)
    '----------------------------------------------
    SkipTest "外部API連携テスト", "外部接続が必要なためスキップ"
End Sub

実務版コードの解説

テストの自動発見の仕組み

実務版の最大の特徴は、CollectTestProcedures関数によるテストの自動発見機能です。


' VBProject内の全モジュールをスキャン
For Each vbComp In vbProj.VBComponents
    If vbComp.Type = 1 Then  ' 標準モジュールのみ
        Set codeMod = vbComp.CodeModule
        ' プロシージャ名がTest_で始まるものを収集
        If Left(procName, Len(TEST_PREFIX)) = TEST_PREFIX Then
            ' テスト対象として登録
        End If
    End If
Next vbComp

この仕組みにより、新しいテストを追加するときは Test_ で始まるSubプロシージャを書くだけで自動的に検出されます。テストランナー側の修正は不要です。

注意:この機能を使うには、「VBAプロジェクトオブジェクトモデルへのアクセスを信頼する」設定が必要です。Excel の「ファイル」→「オプション」→「トラストセンター」→「トラストセンターの設定」→「マクロの設定」で、この項目にチェックを入れてください。

処理時間の計測

各テストの実行時間を QueryPerformanceCounter APIで計測しています。これにより、処理が遅いテストを特定できます。

処理時間の計測について詳しくは、マクロの処理時間を計測して高速化のボトルネックを見つける方法を参考にしてください。


' 高精度タイマーAPIを使用
Private Declare PtrSafe Function QueryPerformanceCounter _
    Lib "kernel32" (lpPerformanceCount As Currency) As Long

Timer関数(秒単位)では精度が足りないため、マイクロ秒精度のQueryPerformanceCounterを使用しています。Currency型(64ビット整数)で受け取ることで、LongLong型が使えないVBA環境でも正確に計測できます。

シートへの結果出力

OutputResultsToSheetプロシージャでは、結果を「TestResults」シートに出力します。

出力項目 説明
No. テストの連番
モジュール テストが属するモジュール名
テスト名 Assert関数に渡したテスト名
結果 PASS / FAIL / SKIP / ERROR
期待値 期待した値
実際の値 実際に得られた値
メッセージ 失敗時の補足情報
実行時間 テストの実行時間(ms)

結果は色分けされるので、どこが失敗しているかが一目でわかります。

  • 緑(PASS):テスト合格
  • 赤(FAIL):テスト失敗(行全体が薄い赤背景)
  • 黄(SKIP):スキップ
  • 紫(ERROR):実行時エラー(行全体が薄い紫背景)

エラーハンドリングの工夫

テスト実行時のエラーハンドリングは重要なポイントです。1つのテストでエラーが発生しても、他のテストを継続できるよう設計しています。


Private Sub ExecuteOneTest(ByVal procName As String, _
                           ByVal moduleName As String)
    On Error GoTo ErrHandler
    Application.Run procName
    On Error GoTo 0
    Exit Sub

ErrHandler:
    ' エラーを記録して次のテストに進む
    RecordError procName, _
                "実行時エラー " & Err.Number & ": " & Err.Description
    On Error GoTo 0
End Sub

エラー処理の基本パターンについては、エラー処理で止まらないマクロを作る方法で詳しく解説しています。また、リトライやログ出力を組み合わせた応用パターンは、エラー処理の応用パターン(リトライ・ログ・通知)を実装する方法で紹介しています。

AssertError の使い方

AssertErrorは、エラーが「発生すること」をテストする特殊なAssertです。使い方にはコツがあります。


Public Sub Test_ゼロ除算のエラーテスト()
    Dim result As Double

    ' On Error Resume Next でエラーを捕捉
    On Error Resume Next
    result = 1 / 0          ' ゼロ除算エラーが発生するはず
    AssertError "ゼロ除算エラー", 11  ' エラー番号11を期待
    On Error GoTo 0
End Sub

ポイントは、On Error Resume Next でエラーを握りつぶした直後に AssertError を呼ぶことです。AssertErrorErr.Number を確認して、期待したエラー番号と一致するかを検証します。

よくある落とし穴と対処法

落とし穴1:VBProjectへのアクセスが拒否される

症状CollectTestProceduresでエラー「プログラミングによるVisual Basicプロジェクトへのアクセスは信頼性に欠けます」が出る。

対処法

  1. Excelの「ファイル」→「オプション」を開く
  2. 「トラストセンター」→「トラストセンターの設定」をクリック
  3. 「マクロの設定」を選択
  4. 「VBAプロジェクトオブジェクトモデルへのアクセスを信頼する」にチェック
  5. OKで閉じる

この設定変更は会社のセキュリティポリシーで制限されている場合があります。その場合は、最小版のように手動でテストプロシージャを列挙する方法を使ってください。

落とし穴2:テスト内でシートを操作するとテスト同士が干渉する

症状:テスト単体では成功するが、全テストを一括実行すると失敗するテストがある。

原因:前のテストが変更したシートの状態が、次のテストに影響している。

対処法:テスト内でシートを操作する場合は、テストの開始時に初期化(セットアップ)、終了時に後片付け(ティアダウン)を行います。


Public Sub Test_シート操作_セル書き込み()
    ' セットアップ:テスト用シートを初期化
    Dim ws As Worksheet
    Set ws = ThisWorkbook.Worksheets("Sheet1")
    ws.Range("A1:Z100").ClearContents

    ' テスト実行
    ws.Range("A1").Value = "テスト"
    AssertEqual "セル書き込み", "テスト", ws.Range("A1").Value

    ' ティアダウン:テスト用データを消去
    ws.Range("A1:Z100").ClearContents
End Sub

落とし穴3:浮動小数点の比較で失敗する

症状:計算結果が正しいはずなのに AssertEqual が失敗する。

原因:浮動小数点演算の誤差。例えば 0.1 + 0.2 は正確に 0.3 にならない。

対処法:浮動小数点を比較する場合は、許容誤差を設けた比較を行います。


Public Sub AssertAlmostEqual(ByVal testName As String, _
                              ByVal expected As Double, _
                              ByVal actual As Double, _
                              Optional ByVal tolerance As Double = 0.0001)
    '----------------------------------------------
    ' 浮動小数点の近似一致を判定する
    '----------------------------------------------
    If Abs(expected - actual) <= tolerance Then
        ' PASS処理
    Else
        ' FAIL処理
    End If
End Sub

落とし穴4:Application.Runで日本語プロシージャ名が動かない

症状:テストプロシージャ名に日本語を使うと Application.Run でエラーになるケースがある。

原因:Excelのバージョンや環境によっては、Application.Runに日本語名を渡すと文字化けすることがある。

対処法:テストプロシージャ名はアルファベットのみにするか、モジュール名を明示的に指定します。


' モジュール名を含めた完全修飾名で呼び出す
Application.Run "MTests.Test_CalcTax_Rate10"

安全策として、プロシージャ名自体はアルファベットで書き、Assert関数のテスト名(第1引数)に日本語の説明を入れるスタイルがおすすめです。

落とし穴5:テスト結果シートが保護されている

症状:テスト結果の出力時にエラーが発生する。

原因:「TestResults」シートまたはブックにシート保護やブック保護がかかっている。

対処法:結果出力前に保護を解除するコードを追加します。


' シート保護の解除
On Error Resume Next
ws.Unprotect Password:=""
On Error GoTo 0

落とし穴6:テスト対象のコードがPrivateで参照できない

症状:Private宣言された関数やサブルーチンをテストから呼べない。

対処法:テスト対象の関数を Public にするか、テスト用のラッパー関数を作ります。テスト可能性を考慮して、なるべくPublicの小さな関数に分割する設計が望ましいです。

落とし穴7:大量テストの実行でメモリ不足になる

症状:テスト数が数百件を超えると、実行が極端に遅くなるか、メモリ不足エラーが発生する。

対処法ReDim Preserve を毎回呼んでいると、配列の再確保でパフォーマンスが低下します。事前に大きめの配列を確保するか、一定件数ごとに結果を出力してメモリを解放します。


' 事前に十分なサイズを確保
ReDim mResults(1 To 1000)  ' 最大1000件
' 実際の件数はmResultCountで管理

VBAのテスト自動実行でVBProjectアクセスが拒否されるときの対処法

「プログラミングによるVisual Basicプロジェクトへのアクセスは信頼性に欠けます」エラーが出る場合、トラストセンターの設定が原因だ。「ファイル」→「オプション」→「トラストセンター」→「マクロの設定」で「VBAプロジェクトオブジェクトモデルへのアクセスを信頼する」にチェックを入れよう。会社のポリシーで制限されている場合は手動列挙方式を使う。

VBAのテストが一括実行で失敗するときの対処法

「テスト単体ではPASSするのに全テスト実行だとFAILになる」場合、テスト同士がシートの状態を共有していて前のテストの結果が次に影響していることが原因だ。各テストの冒頭でシートを初期化するSetup処理を入れ、テスト終了後にTeardownでクリーンアップする仕組みを導入しよう。

よくある質問(FAQ)

Q1:テストはいつ実行すればよいですか?

A1:最低限、以下のタイミングで実行してください。

  • コードを修正したとき(回帰テスト)
  • 新しい機能を追加したとき(新機能の動作確認 + 既存機能の回帰テスト)
  • 本番環境に反映する前(リリース前テスト)

理想的には、修正するたびに毎回全テストを実行する習慣をつけるのがベストです。自動テストなら数秒で完了するので、負担にはなりません。

Q2:テストケースはどのくらい書けばよいですか?

A2:最低限、以下の3パターンを網羅しましょう。

  1. 正常系:通常の入力でのテスト(最も基本)
  2. 境界値:0、空文字、最大値、最小値など端の値
  3. 異常系:不正な入力(マイナス値、型違い、Null等)

すべての関数にテストを書く必要はありません。まずはバグが起きると被害が大きい重要な関数(金額計算、日付処理など)から始めてください。

Q3:テスト対象の関数がシートのデータに依存しています。どうすればよいですか?

A3:テスト用のデータをテストコード内でセットアップする方法が確実です。


Public Sub Test_集計関数()
    Dim ws As Worksheet
    Set ws = ThisWorkbook.Worksheets("Sheet1")

    ' セットアップ:テスト用データを書き込む
    ws.Range("A1").Value = 100
    ws.Range("A2").Value = 200
    ws.Range("A3").Value = 300

    ' テスト実行
    AssertEqual "合計", 600, ws.Range("A1").Value + _
                             ws.Range("A2").Value + _
                             ws.Range("A3").Value

    ' ティアダウン:後片付け
    ws.Range("A1:A3").ClearContents
End Sub

または、テスト専用のシートを作成して、そこにテストデータを置く方法もあります。

Q4:MsgBoxやInputBoxが含まれるコードはテストできますか?

A4:MsgBoxやInputBoxはユーザーの操作を待つため、そのままでは自動テストできません。対処法は以下の2つです。

方法1:処理ロジックをUI部分から分離する


' NG:UIとロジックが混在
Sub 処理実行()
    Dim input As String
    input = InputBox("金額を入力")
    MsgBox "税込: " & Int(CLng(input) * 1.1)
End Sub

' OK:ロジックを関数に分離(テスト可能)
Function CalcTaxAmount(ByVal price As Long) As Long
    CalcTaxAmount = Int(price * 1.1)
End Function

Sub 処理実行()
    Dim input As String
    input = InputBox("金額を入力")
    MsgBox "税込: " & CalcTaxAmount(CLng(input))
End Sub

方法2:テスト時はMsgBoxを表示しないフラグを用意する


Public gIsTestMode As Boolean

Sub ShowResult(ByVal msg As String)
    If Not gIsTestMode Then
        MsgBox msg
    End If
End Sub

Q5:テストを特定のモジュールだけ実行することはできますか?

A5CollectTestProcedures関数にフィルター条件を追加すれば可能です。例えば、モジュール名で絞り込む関数を作ります。


Public Sub RunTestsInModule(ByVal targetModule As String)
    InitTest

    Dim testProcs() As String
    Dim testModules() As String
    Dim testCount As Long
    testCount = CollectTestProcedures(testProcs, testModules)

    Dim i As Long
    For i = 1 To testCount
        If testModules(i) = targetModule Then
            ExecuteOneTest testProcs(i), testModules(i)
        End If
    Next i

    OutputResultsToSheet 0
End Sub

Q6:テストが失敗したとき、デバッグのコツはありますか?

A6:以下の手順でデバッグするのが効率的です。

  1. テスト結果シートで失敗箇所を確認:「期待値」と「実際の値」の差分を見る
  2. 該当のテストプロシージャにブレークポイントを設定:テスト単体をF5で実行
  3. Debug.Printで中間値を確認Debug.Printとイミディエイトウィンドウで変数の中身を確認する方法を参考に
  4. 期待値が間違っていないか確認:テストコード自体のバグの場合もある

Q7:他のブックの関数もテストできますか?

A7Application.Runにブック名を含めることで、他のブックの関数も呼び出せます。ただし、対象のブックが開いている必要があります。


Dim result As Variant
result = Application.Run("'対象ブック.xlsm'!CalcTax", 1000, 0.1)
AssertEqual "他ブックのCalcTax", 1100, CLng(result)

まとめ

この記事では、VBAでテストを自動化する方法を解説しました。

この記事で学んだこと

項目 内容
Assert関数 AssertEqual / AssertTrue / AssertFalse / AssertNotEqual / AssertError で合否判定
テストケース Test_ で始まるプロシージャとして記述
テストランナー VBProjectのコードモジュールからTest_プロシージャを自動発見・実行
結果レポート 専用シートにPASS/FAIL/SKIP/ERRORを色分けで出力
処理時間計測 QueryPerformanceCounter APIで各テストの実行時間を測定

テスト自動化を成功させるポイント

  1. 小さく始める:まずは最も重要な関数1つのテストから書く
  2. 関数を小さく保つ:テストしやすいコード = 保守しやすいコード
  3. UIとロジックを分離する:ロジック部分を純粋な関数として切り出す
  4. 修正のたびにテスト実行:回帰バグを即座に検出する習慣をつける
  5. テスト名は分かりやすく:失敗時に何が壊れたかすぐ分かる命名を

テスト自動化は最初の導入コストこそかかりますが、コードが増えるほど、修正が増えるほど、その効果は絶大です。「動いてるから大丈夫」から「テストがパスしているから大丈夫」へ。ぜひ一歩を踏み出してみてください。

次のステップ

テスト自動化をマスターしたら、以下の記事でさらにスキルアップしましょう。

コメント

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