【プロ】Access VBAによる「テーブル定義のユニットテスト」:構造の整合性を自動検証する
開発現場で、こんな悪夢を経験したことはないだろうか。
「本番環境にリリースした途端、集計クエリがエラーを吐いた」
「前任者が勝手にフィールドのデータ型を `Long` から `Text` に変更していて、VBAの集計ロジックが沈没した」
「インデックスが消されていて、数百万件のレコードを舐めるバッチ処理が無限に終わらない」
――笑えない話だが、Access開発の現場では日常茶飯事だ。なぜこれほど脆弱なのか?
原因は明白である。「テーブル構造(スキーマ)を目視で確認し、テストを人間が行っているから」だ。
コードの単体テスト(ユニットテスト)は書くくせに、その土台である「テーブル定義」のテストを書く人間は少ない。しかし、Accessという密結合なデータベースにおいて、構造の崩壊はアプリケーション全体の死を意味する。
今回は、DAO(Data Access Objects)を駆使し、「デプロイ前にテーブル構造の整合性を完全自動で検証するユニットテスト基盤」の構築手法を伝授する。
—
なぜ「目視確認」は破綻するのか?
多くの現場では、テーブル設計書の変更履歴と、実際の`.accdb`ファイルを照らし合わせるという泥臭い作業を行っている。だが、これはプロのエンジニアリングとは言えない。理由は3つある。
1. 暗黙の型変換リスク: VBAは型に寛容(Variant等)ゆえに、テーブル側の型不一致を隠蔽し、ある日突然オーバーフローを引き起こす。
2. リレーションのサイレント崩壊: 外部キー制約や参照整合性が、UIの改修時に意図せず外される。
3. インデックスの欠落: パフォーマンス劣化の主要因は、検索キーのインデックス消滅だが、誰も気づけない。
これを解決するには、「コードによって期待するスキーマを定義し、実体をマシンに自動検証させる」仕組み、すなわちテーブル定義のユニットテストが不可欠なのだ。
—
アーキテクチャ設計:テストの全体像
今回構築するテストフレームワークの思想はシンプルだ。
1. 期待値(Expected Schema)をコード内の構造体(User-Defined Type: UDT)または専用の定義用テーブルで保持する。
2. 実体(Actual Schema)をDAOの `TableDef` / `Field` / `Index` コレクションから動的に取得する。
3. 2つを突合し、不一致があれば即座にイミディエイトウィンドウへエラーを出力し、テスト失敗フラグを立てる。
これをデプロイ前やアプリケーション起動時のルーチンに組み込むことで、不正な構造のデータベースを絶対に稼働させない鉄壁の防壁が完成する。
—
プロダクションコード:テーブル定義検証エンジンの実装
以下のコードは、特定のテーブル(例:`T_受注明細`)に対し、「指定したフィールドが存在するか」「データ型が正しいか」「サイズが一致しているか」「必須入力(Required)になっているか」を完全自動検証するプロフェッショナル向けモジュールだ。
標準モジュール(例:`M_TableTest`)として実装してほしい。
Option Explicit
Option Private Module
‘ ==============================================================================
‘ 構造体定義:検証用フィールド仕様
‘ ==============================================================================
Private Type FieldSpec
Name As String
DataType As Long WcDAO.DataTypeEnum
Size As Long
Required As Boolean
End Type
‘ ==============================================================================
‘ ユニットテスト実行エントリポイント
‘ ==============================================================================
Public Sub RunTableDefinitionTests()
Dim isSuccess As Boolean
isSuccess = True
Debug.Print “========================================”
Debug.Print ” テーブル定義ユニットテスト開始”
Debug.Print “========================================”
‘ テストケース1: T_受注明細 の構造検証
If Not AssertTable_T_JuchuMeisai() Then
isSuccess = False
End If
‘ 必要に応じて他のテーブルのテストをここに追加
‘ If Not AssertTable_T_顧客マスタ() Then isSuccess = False Then
Debug.Print “========================================”
If isSuccess Then
Debug.Print ” 【結果】全テスト 成功 (SUCCESS)”
Else
Debug.Print ” 【結果】テスト 失敗 (FAILURE) – 構造に異常があります”
MsgBox “データベースのテーブル構造に不整合が検出されました。” & vbCrLf & _
“詳細はイミディエイトウィンドウを確認してください。”, vbCritical, “スキーマ検証エラー”
End If
Debug.Print “========================================”
End Sub
‘ ==============================================================================
‘ 個別テーブルテスト: T_受注明細
‘ ==============================================================================
Private Function AssertTable_T_JuchuMeisai() As Boolean
Dim db As DAO.Database
Dim tdf As DAO.TableDef
Dim targetTableName As String
Dim specs() As FieldSpec
Dim i As Long
Dim testResult As Boolean
targetTableName = “T_受注明細”
Set db = CurrentDb()
Debug.Print “[TEST] 検証中テーブル: ” & targetTableName
‘ 1. テーブル自体の存在確認
If Not TableExists(db, targetTableName) Then
Debug.Print ” -> [FAIL] テーブルが存在しません: ” & targetTableName
AssertTable_T_JuchuMeisai = False
Exit Function
End If
Set tdf = db.TableDefs(targetTableName)
‘ 2. 期待するフィールド仕様の定義(本来は外部設定ファイルや定数から読み込む)
‘ 引数: フィールド名, DAOデータ型, サイズ, 必須フラグ
specs = SetupExpectedSpecs()
‘ 3. ループによるスキーマ突合検証
testResult = True
For i = LBound(specs) To UBound(specs)
If Not ValidateField(tdf, specs(i)) Then
testResult = False
End If
Next i
AssertTable_T_JuchuMeisai = testResult
End Function
‘ ==============================================================================
‘ 期待値のモック定義(実際のプロジェクトではJSONや別定義テーブルから読込推奨)
‘ ==============================================================================
Private Function SetupExpectedSpecs() As FieldSpec()
Dim specs(3) As FieldSpec
‘ 受注ID (Long Integer, 必須)
specs(0).Name = “受注ID”
specs(0).DataType = dbLong
specs(0).Size = 4
specs(0).Required = True
‘ 商品コード (Text / Short Text, 最大20文字, 必須)
specs(1).Name = “商品コード”
specs(1).DataType = dbText
specs(1).Size = 20
specs(1).Required = True
‘ 数量 (Integer, 必須)
specs(2).Name = “数量”
specs(2).DataType = dbInteger
specs(2).Size = 2
specs(2).Required = True
‘ 備考 (Memo / Long Text, 任意)
specs(3).Name = “備考”
specs(3).DataType = dbMemo
specs(3).Size = 0
specs(3).Required = False
SetupExpectedSpecs = specs
End Function
‘ ==============================================================================
‘ 個別フィールドの検証ロジック
‘ ==============================================================================
Private Function ValidateField(tdf As DAO.TableDef, expected As FieldSpec) As Boolean
Dim fld As DAO.Field
Dim isValid As Boolean
isValid = True
‘ フィールド存在チェック
On Error Resume Next
Set fld = tdf.Fields(expected.Name)
On Error GoTo 0
If fld Is Nothing Then
Debug.Print ” -> [FAIL] フィールドが見つかりません: ” & expected.Name
ValidateField = False
Exit Function
End If
‘ データ型チェック
If fld.Type <> expected.DataType Then
Debug.Print ” -> [FAIL] データ型不一致 [” & expected.Name & “] 期待値:” & GetTypeName(expected.DataType) & ” / 実測値:” & GetTypeName(fld.Type)
isValid = False
End If
‘ サイズチェック(テキスト型などの場合のみ有効)
If expected.DataType = dbText Then
If fld.Size <> expected.Size Then
Debug.Print ” -> [FAIL] フィールドサイズ不一致 [” & expected.Name & “] 期待値:” & expected.Size & ” / 実測値:” & fld.Size
isValid = False
End If
End If
‘ 必須入力チェック
If fld.Required <> expected.Required Then
Debug.Print ” -> [FAIL] 必須設定(Required)不一致 [” & expected.Name & “] 期待値:” & expected.Required & ” / 実測値:” & fld.Required
isValid = False
End If
If isValid Then
Debug.Print ” -> [PASS] ” & expected.Name
End If
ValidateField = isValid
End Function
‘ ==============================================================================
‘ ユーティリティ関数
‘ ==============================================================================
Private Function TableExists(db As DAO.Database, tableName As String) As Boolean
Dim tdf As DAO.TableDef
On Error Resume Next
Set tdf = db.TableDefs(tableName)
TableExists = (Err.Number = 0)
On Error GoTo 0
End Function
Private Function GetTypeName(dataType As Long) As String
Select Case dataType
Case dbBoolean: GetTypeName = “Yes/No (Boolean)”
Case dbByte: GetTypeName = “Byte”
Case dbInteger: GetTypeName = “Integer”
Case dbLong: GetTypeName = “Long Integer”
Case dbCurrency: GetTypeName = “Currency”
Case dbSingle: GetTypeName = “Single”
Case dbDouble: GetTypeName = “Double”
Case dbDate: GetTypeName = “Date/Time”
Case dbText: GetTypeName = “Short Text”
Case dbMemo: GetTypeName = “Long Text (Memo)”
Case Else: GetTypeName = “Other (” & dataType & “)”
End Select
End Function
—
コードの急所:プロが押さえるべき設計のポイント
このコードには、実務でトラブルを回避するための実践的な知見がいくつも組み込まれている。
1. `DAO` ライブラリの明示的な活用
ADOではなく必ず DAO (Data Access Objects) を使用すること。Accessのローカルテーブル定義、インデックス、リレーションシップのメタデータにフルアクセスできるのはDAOだけだ。`CurrentDb()` と組み合わせることで、高速かつ正確にスキーマをメモリ上に展開できる。
2. サイレントエラーの排除 (`On Error Resume Next` の正しい使い方)
フィールドの存在チェック時、存在しないフィールドを `tdf.Fields(“xxx”)` で呼び出すとAccessは実行時エラーを吐いてクラッシュする。
ここではエラーを一時的にトラップ(`On Error Resume Next`)し、オブジェクトが `Nothing` であるかを評価するという、VBAにおける安全な存在確認イディオムを厳密に守っている。直後に `On Error GoTo 0` でエラーハンドリングを復帰させることも忘れてはならない。
3. データ型の厳密な比較
VBAの `dbText`(Short Text)や `dbMemo`(Long Text)は、意図しない型昇格や変更が起きやすい。特にAccessのUI上からユーザーがフィールドサイズを勝手に変更した場合、このテストコードが即座に検知し、イミディエイトウィンドウに「期待値と実測値の差異」を突きつける。
—
現場への導入と運用の極意
このユニットテストをどう運用に組み込むべきか?
- 自動起動マクロ(Autoexec)への組み込み:
アプリケーション起動時、メインフォームが開く前に `RunTableDefinitionTests` をサイレント実行する。もし `isSuccess = False` であれば、即座にアプリケーションを強制終了(`DoCmd.Quit`)させる。これにより、壊れたスキーマでのデータ破損を物理的に阻止できる。
- バックエンド(BE)とフロントエンド(FE)の分離環境における注意:
リンクテーブルを検証する場合、`tdf.Connect` プロパティが空でない(=リンクテーブルである)ことを検知し、リンク先の本物のバックエンド側テーブルの構造を正しく指しているかを考慮する必要がある。本稿のコードは主にローカルテーブル、または開いているデータベース内のテーブル定義を対象としているが、`TableDef.Connect` を拡張することで、外部BEの検証にも応用可能だ。
まとめ:プロフェッショナルな開発とは「疑うこと」から始まる
「動いているから大丈夫」という属人的な安心感は、プロのエンジニアが最も排除すべき甘えである。
Accessという、ともすれば「手抜き開発」の温床になりがちな環境であっても、今回紹介したような「構造の自動検証(ユニットテスト)」をコードベースで組み込んでおけば、組織変更や担当者の変更があっても、システムの整合性は半永久的に保たれる。
明日からの開発に、このテーブル定義テストを導入し、あなたのシステムを「壊れない要塞」へと進化させてほしい。
