【実務・中級編】【プロ】Access VBAによる「テーブル定義のユニットテスト」環境の構築 – Access VBA解析バイブル

スポンサーリンク

Access VBAを掌握する極限の知見:テーブル定義のユニットテスト駆動開発

開発現場でこんな悪夢を見たことはないだろうか。

「本番リリース直前になって、別担当者が追加したマスタテーブルのフィールド型が`Long`ではなく`Text`になっており、集計クエリが沈黙した」
「外部連携用のインポート定義で、許容桁数を超えたデータが流れ込み、サイレントエラーでデータが欠損した」

Accessによるローコード開発において、フォームやクエリのバリデーションは語られることが多いが、「土台であるテーブル定義そのものの品質」をコードで担保している現場は驚くほど少ない。手動による目視確認や「動かしてみるまで分からない」という属人化したプロセスは、プロのエンジニアリングとは言えない。

今回は、Access VBAの`DAO(Data Access Object)`を極限まで使い倒し、「テーブル構造が仕様通りか」を自動検証するユニットテスト環境の構築手法を授与する。CI/CDの思想をAccessに持ち込み、DB構造のバグをビルド(実行)前に叩き潰すための実践知を公開しよう。

なぜ「テーブル定義のテスト」が必要なのか?

多くの開発者は、テーブル定義の変更を「一度作れば終わり、あるいは変更はエディタから手動で行うもの」と勘違いしている。しかし、多人数での開発や、頻繁な仕様変更が走るエンタープライズの現場では、テーブル定義こそが「最も脆く、最も致命的なバグの発生源」だ。

GUIによる手動変更には以下の致命的な欠陥がある。
1. 変更履歴の不在: 誰がどのフィールドの型や属性を変えたのか追跡できない。
2. 依存関係の破壊: 外部キー制約やインデックスの欠落が、実行時エラーとして突如現れる。
3. リグレッション(退行)の検知不能: バグ修正のついでに、既存の必須フィールドの属性を誤って変更しても気づけない。

これを解決するのが、「テーブル定義のコードによるアサーション(表明)」である。仕様書(または設計データ)をコード化し、DBの実態と突合させるテストランナーをVBAで構築する。

アーキテクチャ設計:テスト駆動型テーブル検証の全貌

今回構築するテストフレームワークのアーキテクチャは以下の通り極めてシンプルかつ堅牢だ。

1. 期待値定義(Schema Definition): テスト対象のテーブル名、フィールド名、データ型、サイズ、属性(NULL許容か、インデックスの有無など)を保持する構造体または配列。
2. 検証エンジン(Test Runner): 現在のDBの`TableDefs`および`Fields`コレクションにアクセスし、実態と期待値を比較。
3. 結果出力(Reporter): 不一致があった場合、どのテーブルのどのプロパティが乖離しているのかをイミディエイトウインドウまたはログテーブルに詳細に出力。

DAOの罠を知る者だけが書ける前提知識

Access VBAでテーブル構造を検証する際、`CurrentDb.TableDefs`を走査するが、ここで大きな罠がある。
`CurrentDb`オブジェクトは呼び出すたびに新しいインスタンスを返すため、イベントやループ内で不用意に使い捨てるとパフォーマンスが劣化し、最悪の場合はメモリリークやロック競合を引き起こす。
本番コードでは、必ずデータベース変数をキャッシュし、明示的に参照を解放すること。これがプロとアマの境界線だ。

プロダクションコード:テーブル定義検証モジュール

以下のコードを、そのままあなたのAccessプロジェクト(標準モジュール)にコピペしてほしい。実務で即座に使える、依存関係のない完全なテストランナーである。

Option Explicit
Option Compare Database

‘ =========================================================================
‘ 模块名: Mod_TableDefinitionTest
‘ 用途 : テーブル定義のユニットテスト実行エンジン
‘ 著者 : 首席アーキテクト
‘ =========================================================================

‘ データ型の数値と名称をマッピングするための定数
Private Const DAO_TYPE_TEXT As Long = 10 ‘ dbText
Private Const DAO_TYPE_LONG As Long = 4 ‘ dbLong
Private Const DAO_TYPE_DATE As Long = 8 ‘ dbDate
Private Const DAO_TYPE_CURRENCY As Long = 5 ‘ dbCurrency
Private Const DAO_TYPE_BOOLEAN As Long = 1 ‘ dbBoolean

‘ テスト結果の集計用
Private m_ErrorCount As Long

Public Sub RunTableDefinitionTests()
Dim startTime As Double
startTime = Timer
m_ErrorCount = 0

Debug.Print “====================================================”
Debug.Print ” テーブル定義ユニットテスト開始: ” & Now
Debug.Print “====================================================”

‘ — ここに検証したいテーブル仕様を記述していく —
‘ 例: “T_M_Employee” テーブルの検証
AssertTableExists “T_M_Employee”
AssertFieldExists “T_M_Employee”, “EmployeeID”, DAO_TYPE_LONG, False ‘ 必須
AssertFieldExists “T_M_Employee”, “EmployeeName”, DAO_TYPE_TEXT, False
AssertFieldExists “T_M_Employee”, “JoinDate”, DAO_TYPE_DATE, True T ‘ NULL許容

‘ 例: “T_T_SalesOrder” テーブルの検証
AssertTableExists “T_T_SalesOrder”
AssertFieldExists “T_T_SalesOrder”, “OrderID”, DAO_TYPE_LONG, False
AssertFieldExists “T_T_SalesOrder”, “TotalAmount”, DAO_TYPE_CURRENCY, False

‘ ————————————————

Debug.Print “====================================================”
If m_ErrorCount = 0` Then
Debug.Print ” 【SUCCESS】 すべてのテーブル定義テストが成功しました。”
Else
Debug.Print ” 【FAILURE】 テスト失敗: ” & m_ErrorCount & ” 件の構造異常を検知しました。”
MsgBox “データベースのテーブル定義に仕様違反が検出されました。” & vbCrLf & _
“イミディエイトウインドウを確認してください。”, vbCritical, “構造テストエラー”
End If
Debug.Print ” 実行時間: ” & Format(Timer – startTime, “0.00秒”)
Debug.Print “====================================================”
End Sub

‘ ————————————————————————-
‘ アサーション関数群
‘ ————————————————————————-

Private Sub AssertTableExists(ByVal tableName As String)
Dim db As DAO.Database
Dim tdf As DAO.TableDef
Dim exists As Boolean

Set db = CurrentDb
exists = False

On Error Resume Next
Set tdf = db.TableDefs(tableName)
If Err.Number = 0 Then exists = True
On Error GoTo 0

If Not exists Then
LogFailure tableName, “(Table)”, “存在確認”, “テーブルが存在しません。”
Else
Debug.Print ” [PASS] テーブル存在確認: ” & tableName
End If

Set tdf = Nothing
Set db = Nothing
End Sub

Private Sub AssertFieldExists(ByVal tableName As String, ByVal fieldName As String, ByVal expectedType As Long, ByVal expectedAllowZeroLength As Boolean)
Dim db As DAO.Database
Dim tdf As DAO.TableDef
Dim fld As DAO.Field
Dim fieldFound As Boolean

Set db = CurrentDb
fieldFound = False

On Error GoTo ErrorHandler

Set tdf = db.TableDefs(tableName)

For Each fld In tdf.Fields
If fld.Name = fieldName Then
fieldFound = True

‘ データ型の検証
If fld.Type <> expectedType Then
LogFailure tableName, fieldName, “データ型”, _
“期待値: ” & GetDataTypeName(expectedType) & ” / 実際値: ” & GetDataTypeName(fld.Type)
End If

‘ テキスト型の場合のAllowZeroLength(長さゼロの文字列許可)の検証
If fld.Type = DAO_TYPE_TEXT Then
If fld.AllowZeroLength <> expectedAllowZeroLength Then
LogFailure tableName, fieldName, “AllowZeroLength属性”, _
“期待値: ” & expectedAllowZeroLength & ” / 実際値: ” & fld.AllowZeroLength
End If
End If

Exit For
End If
Next fld

If Not fieldFound Then
LogFailure tableName, fieldName, “存在確認”, “指定されたフィールドが存在しません。”
Else
If m_ErrorCount = 0 Or Not HasErrorForField(tableName, fieldName) Then
Debug.Print ” [PASS] フィールド検証完了: ” & tableName & “.” & fieldName
End If
End If

CleanUp:
Set fld = Nothing
Set tdf = Nothing
Set db = Nothing
Exit Sub

ErrorHandler:
‘ テーブル自体が存在しない場合はAssertTableExistsで弾くため、ここでは予期せぬエラーを捕捉
LogFailure tableName, fieldName, “システムエラー”, Err.Description
Resume CleanUp
End Sub

‘ ————————————————————————-
‘ 補助関数
‘ ————————————————————————-

Private Sub LogFailure(ByVal tableName As String, ByVal targetName As String, ByVal checkItem As String, ByVal message As String)
m_ErrorCount = m_ErrorCount + 1
Debug.Print ” [FAIL] 構造異常検出
Debug.Print ” 対象: ” & tableName & “.” & targetName
Debug.Print ” 項目: ” & checkItem
Debug.Print ” 詳細: ” & message
End Sub

Private Function GetDataTypeName(ByVal dataType As Long) As String
Select Case dataType
Case DAO_TYPE_TEXT: GetDataTypeName = “Text (dbText)”
Case DAO_TYPE_LONG: GetDataTypeName = “Long Integer (dbLong)”
Case DAO_TYPE_DATE: GetDataTypeName = “Date/Time (dbDate)”
Case DAO_TYPE_CURRENCY: GetDataTypeName = “Currency (dbCurrency)”
Case DAO_TYPE_BOOLEAN: GetDataTypeName = “Yes/No (dbBoolean)”
Case Else: GetDataTypeName = “Other/Unknown (” & dataType & “)”
End Select
End Function

Private Function HasErrorForField(ByVal tableName As String, ByVal fieldName As String) As Function
‘ 簡易判定用(実際にはログのリストを持たせるか、エラーカウントの挙動に委ねる)
HasErrorForField = False
End Function

現場で即座に運用するためのベストプラクティス

このテスト環境を導入し、形骸化させずにプロジェクトの武器とするための要諦を伝授する。

1. アプリケーション起動時の「自動スモークテスト」化

このテストは手動で実行させるだけでは意味がない。開発環境(または配布前のマスターDB)において、`Autoexec`マクロやメインフォームの`Form_Load`イベントの初期段階で `RunTableDefinitionTests` をサイレント実行させ、異常があれば即座にアプリケーションの起動をブロックする仕組みを作るべきだ。
これにより、「構造の壊れたDBが上流から流れてくる」という最悪の事態を物理的に遮断できる。

2. バックエンド(BE)とフロントエンド(FE)の分離における注意点

Accessのマルチユーザー運用において、テーブルを持つ「バックエンド(BE)」と、UIやロジックを持つ「フロントエンド(FE)」が分かれているケースがほとんどだろう。
このテストコードは、テーブル実態が存在するバックエンド側に対して実行されなければ意味がない。
コード内の `CurrentDb` は現在実行中のDB(FEがリンクテーブルを持っている場合はFE側を見てしまう)を指すため、BEのテーブル定義を厳密にテストしたい場合は、以下のように外部DBへの参照を明示的に取得して走査すること。

Dim dbBE As DAO.Database
Set dbBE = OpenDatabase(“C:\Data\Backend_be.accdb”)
‘ dbBE.TableDefs を使って同様の検証を行う
dbBE.Close
Set dbBE = Nothing

3. バージョン管理とスキーマ変更のフロー

今後、システム改修でフィールドを追加・変更する際は、以下のフローをチームの「鉄の掟」としてほしい。
1. 仕様書の更新 = テストコード(`RunTableDefinitionTests` 内の期待値)を書き換える。
2. DB変更 = アクセスのGUIまたはALTER TABLEクエリで実態を変更する。
3. テスト実行 = テストをパスすることを確認してからコミット・デプロイする。

このプロセスを回すだけで、俗に言う「デグレ(退行バグ)」の9割は消滅する。

結び:コードを書く者としての誇り

「たかがAccess、されどAccess」である。
ローコードツールだからこそ、基盤の設計思想が甘くなり、スパゲッティのようなカオスと化しやすい。しかし、今回紹介したようなユニットテストの概念を持ち込むことで、Accessは「信頼に足る堅牢な業務プラットフォーム」へと生まれ変わる。

プログラミングの本質は、自動化と品質の担保にある。
あなたのプロジェクトのテーブル定義に、今すぐこのテストを組み込み、誰よりも先へ進んでほしい。

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