[Access VBA] フォームのコントロール操作系の関数をどのようにユニットテストするかの方法メモ

概要

この記事について

かんたんな概要と結論

動的なフォーム生成、および動的なフォーム上のコントロール生成によって
実際に使用するフォームなどの環境から切り離して
コントロール操作系の関数のユニットテストができる。

MSAccessのVBAで安定性のあるアプリケーションを作成する場合、
関数を機能単位で分割してユニットテストすると安定性が高まる。

純粋な関数の場合はテストは簡単だが、
関数の外部のグローバル変数やコンポーネント
(フォームのテキストボックスの値や背景色など)を変更する場合はユニットテストが難しくなり、
また、使用するフォームやクエリに何かしらの影響を与えて、それらを汚す(想定外の挙動を付与する)かもしれない。

フォームのコントロール操作系の関数にしぼって考えると、
VBAによってフォームやその上のコントロールを動的に生成し、
後始末をちゃんとすることで、
コード上でテスト環境の作成まですべて完結、かつ環境をなるべく汚さない方法でユニットテストできるだろう。

その方法を記したい。

説明のために作成したサンプルを含むツール(Accessファイル)とソースコードはこちらでダウンロードできます。

作成環境

  • windows10
  • MSOffice2019

想定するケース

About

次のように、

  1. ボタン内容でプルダウンのリストを切り替える関数
  2. ボタン内容で入力欄の使用可能/不可能を切り替える関数

の機能をそれぞれもつフレームがあるMainフォームを考える。

サンプル

それぞれのフレームでは、
内部のラジオボタンの切り替えにより、
コンボボックスのプルダウンのリストを食べ物から飲み物のリストに切り替えたり、
使用可能な入力欄を18歳未満専用のものから18歳以上用のものに切り替えたりする(コードは後述)。

フレームのイベントから呼び出される関数の処理は、
操作するコントロールに強く結びついているため、
Mainフォーム上でテストするとMainフォーム自体の何かを変更する可能性がある。

コード

【サンプル①】

 1
 2'******************************************************************************************
 3'*機能      :コンボボックスの項目リストを変更
 4'*引数      :
 5'******************************************************************************************
 6Public Sub changeCmbBoxItems(ByVal selectedNumber As Long, ByVal cmbBox As ComboBox)
 7    
 8    '定数
 9    Const FUNC_NAME As String = "changeCmbBoxItems"
10    
11    '変数
12    
13    On Error GoTo ErrorHandler
14    
15    '//項目のクリア
16    cmbBox.RowSource = ""
17    
18    Select Case selectedNumber
19    '//食べ物
20    Case 1
21        cmbBox.AddItem "ピザ"
22        cmbBox.AddItem "そば"
23        cmbBox.AddItem "焼き肉"
24    '//飲み物
25    Case 2
26        cmbBox.AddItem "コーラ"
27        cmbBox.AddItem "緑茶"
28        cmbBox.AddItem "水"
29    End Select
30
31ExitHandler:
32
33    Exit Sub
34    
35ErrorHandler:
36
37    MsgBox "エラーが発生したため、マクロを終了します。" & _
38           vbLf & _
39           "関数名:" & FUNC_NAME & _
40           vbLf & _
41           "クラス名:" & SOURCE_NAME & _
42           vbLf & _
43           "エラー番号:" & Err.Number & vbNewLine & _
44           Err.Description, vbCritical
45        
46    GoTo ExitHandler
47        
48End Sub
49

【サンプル②】

 1'******************************************************************************************
 2'*機能      :テキストボックスの使用可能状態を変更
 3'*引数      :
 4'******************************************************************************************
 5Public Sub changeTextBoxesEnabled(ByVal selectedNumber As Long, ByRef textboxes() As textbox)
 6    
 7    '定数
 8    Const FUNC_NAME As String = "changeTextBoxesEnabled"
 9    
10    '変数
11    Dim canUnder18Enable As Boolean '//18歳未満のためのテキストボックスが有効かどうか
12    Dim textbox As Variant
13    
14    On Error GoTo ErrorHandler
15    
16    '//18歳未満を選択時はTrue、それ以外の場合はFalse
17    canUnder18Enable = (selectedNumber = 1)
18    
19    '//タグがunder18かover18かによって
20    '//使用可能状態を切り替える
21    For Each textbox In textboxes
22        If InStr(textbox.Tag, "under18") <> 0 Then
23            textbox.Enabled = canUnder18Enable
24        Else
25            textbox.Enabled = Not canUnder18Enable
26        End If
27    Next textbox
28
29ExitHandler:
30
31    Exit Sub
32    
33ErrorHandler:
34
35    MsgBox "エラーが発生したため、マクロを終了します。" & _
36           vbLf & _
37           "関数名:" & FUNC_NAME & _
38           vbLf & _
39           "クラス名:" & SOURCE_NAME & _
40           vbLf & _
41           "エラー番号:" & Err.Number & vbNewLine & _
42           Err.Description, vbCritical
43        
44    GoTo ExitHandler
45        
46End Sub

ユニットテストの方法

About

動的なフォーム、コントロール作成により、
テスタブルな環境を即時作成し、すぐに削除するようにする。

その際に、環境構築の順序を気をつけないとエラーが頻出したため、
気をつけなければならなかったこととしてそれを記した。

テストコード

【サンプル①のテスト】

 1'******************************************************************************************
 2'*機能      :テスト コンボボックスの項目リストを変更関数
 3'******************************************************************************************
 4Public Sub テスト_changeCmbBoxItems()
 5    
 6    '定数
 7    Const FUNC_NAME As String = "テスト_changeCmbBoxItems"
 8    
 9    '変数
10    Dim tForm As Form
11    Dim fName As String
12    Dim cmb As ComboBox
13    
14    On Error GoTo ErrorHandler
15
16    '//フォームの動的作成
17    Set tForm = CreateForm()
18    fName = tForm.Name
19    
20    '//デザインビューで開く
21    DoCmd.OpenForm fName, acDesign
22    
23    '//コンボボックスの動的作成
24    Set cmb = CreateControl(fName, _
25                            AcControlType.acComboBox)
26    Dim mycmb As String
27    mycmb = "mycmb"
28    cmb.Name = mycmb
29    cmb.RowSourceType = "Value List"
30    
31    '//デザインビューを閉じる
32    DoCmd.Close acForm, fName, acSaveYes
33    
34    '//フォームビューで開く
35    DoCmd.OpenForm fName, acNormal
36    
37    '//上記で作成したコンボボックスを再度参照
38    Set cmb = Forms(fName).Controls(mycmb)
39    
40    '//■テスト01:食べ物のリスト設定
41    '////関数呼び出し
42    Call changeCmbBoxItems(1, cmb)
43    '////アサーション
44    Debug.Assert cmb.ListCount = 3
45    Debug.Assert cmb.Column(0, 0) = "ピザ"
46    Debug.Assert cmb.Column(0, 1) = "そば"
47    Debug.Assert cmb.Column(0, 2) = "焼き肉"
48        
49    '//■テスト02:飲み物のリスト設定
50    '////関数呼び出し
51    Call changeCmbBoxItems(2, cmb)
52    '////アサーション
53    Debug.Assert cmb.ListCount = 3
54    Debug.Assert cmb.Column(0, 0) = "コーラ"
55    Debug.Assert cmb.Column(0, 1) = "緑茶"
56    Debug.Assert cmb.Column(0, 2) = "水"
57    
58    '//フォームビューを閉じる
59    DoCmd.Close , , acSaveNo
60    
61    '//動的生成したフォームを削除
62    DoCmd.DeleteObject acForm, fName
63    
64ExitHandler:
65    
66    '//テスト完了
67    Debug.Print Now & ":Finish " & FUNC_NAME
68    
69    Exit Sub
70    
71ErrorHandler:
72
73    MsgBox "エラーが発生したため、マクロを終了します。" & _
74           vbLf & _
75           "関数名:" & FUNC_NAME & _
76           vbLf & _
77           "クラス名:" & SOURCE_NAME & _
78           vbLf & _
79           "エラー番号:" & Err.Number & vbNewLine & _
80           Err.Description, vbCritical
81        
82    GoTo ExitHandler
83        
84End Sub

【サンプル②のテスト】

  1'******************************************************************************************
  2'*機能      :テスト テキストボックスの使用可能状態の変更関数
  3'******************************************************************************************
  4Public Sub テスト_changeTextBoxesEnabled()
  5    
  6    '定数
  7    Const FUNC_NAME As String = "テスト_changeTextBoxesEnabled"
  8    
  9    '変数
 10    Dim tForm As Form
 11    Dim fName As String
 12    Dim textboxes(0 To 3) As textbox
 13    Dim i As Long
 14    
 15    On Error GoTo ErrorHandler
 16
 17    '//フォームの動的作成
 18    Set tForm = CreateForm()
 19    fName = tForm.Name
 20    
 21    '//デザインビューで開く
 22    DoCmd.OpenForm fName, acDesign
 23    
 24    '//テキストボックス配列の動的作成
 25    For i = 0 To 3
 26        Set textboxes(i) = CreateControl(fName, _
 27                            AcControlType.acTextBox)
 28                            
 29        textboxes(i).Name = "mytext_" & i
 30        
 31        '//一部のみunder18、それ以外はover18のタグを付与
 32        If i < 2 Then
 33            textboxes(i).Tag = "under18"
 34        Else
 35            textboxes(i).Tag = "over18"
 36        End If
 37    Next i
 38    
 39    '//デザインビューを閉じる
 40    DoCmd.Close acForm, fName, acSaveYes
 41    
 42    '//フォームビューで開く
 43    DoCmd.OpenForm fName, acNormal
 44    
 45    '//上記で作成したテキストボックス配列を再度参照
 46    For i = 0 To 3
 47        Set textboxes(i) = Forms(fName).Controls("mytext_" & i)
 48    Next i
 49    
 50    '//■テスト01:18歳未満専用のテキストボックスの有効化
 51    '////関数呼び出し
 52    Call changeTextBoxesEnabled(1, textboxes)
 53    '////アサーション
 54    Debug.Assert textboxes(0).Tag = "under18"
 55    Debug.Assert textboxes(0).Enabled = True
 56    Debug.Assert textboxes(1).Tag = "under18"
 57    Debug.Assert textboxes(1).Enabled = True
 58    Debug.Assert textboxes(2).Tag <> "under18"
 59    Debug.Assert textboxes(2).Enabled = False
 60    Debug.Assert textboxes(3).Tag <> "under18"
 61    Debug.Assert textboxes(3).Enabled = False
 62    
 63    '//■テスト02:18歳以上専用のテキストボックスの有効化
 64    '////関数呼び出し
 65    Call changeTextBoxesEnabled(2, textboxes)
 66    '////アサーション
 67    Debug.Assert textboxes(0).Tag = "under18"
 68    Debug.Assert textboxes(0).Enabled = False
 69    Debug.Assert textboxes(1).Tag = "under18"
 70    Debug.Assert textboxes(1).Enabled = False
 71    Debug.Assert textboxes(2).Tag <> "under18"
 72    Debug.Assert textboxes(2).Enabled = True
 73    Debug.Assert textboxes(3).Tag <> "under18"
 74    Debug.Assert textboxes(3).Enabled = True
 75        
 76    '//フォームビューを閉じる
 77    DoCmd.Close , , acSaveNo
 78    
 79    '//動的生成したフォームを削除
 80    DoCmd.DeleteObject acForm, fName
 81    
 82ExitHandler:
 83
 84    '//テスト完了
 85    Debug.Print Now & ":Finish " & FUNC_NAME
 86    
 87    Exit Sub
 88    
 89ErrorHandler:
 90
 91    MsgBox "エラーが発生したため、マクロを終了します。" & _
 92           vbLf & _
 93           "関数名:" & FUNC_NAME & _
 94           vbLf & _
 95           "クラス名:" & SOURCE_NAME & _
 96           vbLf & _
 97           "エラー番号:" & Err.Number & vbNewLine & _
 98           Err.Description, vbCritical
 99        
100    GoTo ExitHandler
101        
102End Sub
103
104

気をつけなければならなかったこと

ビューによって設定可能な部分が異なる

私自身まだあまりAccessのビューごとの性質の違いについて
十分に把握していないため、
エラーのトラブルシューティングに見舞われることになった。

 1'//デザインビューで開く
 2DoCmd.OpenForm fName, acDesign
 3
 4'//コンボボックスの動的作成
 5Set cmb = CreateControl(fName, _
 6                        AcControlType.acComboBox)
 7Dim mycmb As String
 8mycmb = "mycmb"
 9cmb.Name = mycmb
10cmb.RowSourceType = "Value List"
11
12'//デザインビューを閉じる
13DoCmd.Close acForm, fName, acSaveYes

NameRowSourceTypeの指定は
デザインビューでないと機能しない
(本当は何か回避策があるかもしれないが、私のコードだとそうなった)ため、
このようにしてデザインビューにおいて開閉することでプロパティをテスタブルに設定した。

同様に、
次のようにコンボボックスのリスト項目を変更して参照する場合も
フォームビューで開いておかないとエラーとなるため、
次のようにする。

 1'//フォームビューで開く
 2DoCmd.OpenForm fName, acNormal
 3
 4'//上記で作成したコンボボックスを再度参照
 5Set cmb = Forms(fName).Controls(mycmb)
 6
 7'//■テスト01:食べ物のリスト設定
 8'////関数呼び出し
 9Call changeCmbBoxItems(1, cmb)
10'////アサーション
11Debug.Assert cmb.ListCount = 3
12Debug.Assert cmb.Column(0, 0) = "ピザ"
13Debug.Assert cmb.Column(0, 1) = "そば"
14Debug.Assert cmb.Column(0, 2) = "焼き肉"
15    
16'//■テスト02:飲み物のリスト設定
17'////関数呼び出し
18Call changeCmbBoxItems(2, cmb)
19'////アサーション
20Debug.Assert cmb.ListCount = 3
21Debug.Assert cmb.Column(0, 0) = "コーラ"
22Debug.Assert cmb.Column(0, 1) = "緑茶"
23Debug.Assert cmb.Column(0, 2) = "水"
24
25'//フォームビューを閉じる
26DoCmd.Close , , acSaveNo

ビュー変更の際に参照がリセットされるため、再度参照を設定し直す

次のように、
フォームビューを開いた後に変数cmbのコンボボックスに対しての参照を
復旧させないといけない。

16行目を怠ると、
リセットによりcmbはNull参照をしているためエラーが発生する。

 1'//デザインビューで開く
 2DoCmd.OpenForm fName, acDesign
 3
 4'//コンボボックスの動的作成
 5Set cmb = CreateControl(fName, _
 6                        AcControlType.acComboBox)
 7......
 8
 9'//デザインビューを閉じる
10DoCmd.Close acForm, fName, acSaveYes
11
12'//フォームビューで開く
13DoCmd.OpenForm fName, acNormal
14
15'//上記で作成したコンボボックスを再度参照
16Set cmb = Forms(fName).Controls(mycmb)

実行

上記テストコードを実行すると、
自動的に新規フォームが作成され、
フォーム上にテストに必要なコンポーネント(コンボボックス、テキストボックス数個)が整えられる。

作成した関数が実行され、適切な結果かどうかをDebug.Assertメソッドで評価。
もし想定通りならば実行は一時停止せずにそのまま処理される。

最後にフォームが削除され、
環境を汚さずにテストが終了する。

まとめ

コントロールを動的に設定する際に
いくつか気をつけないとエラーを吐くのは
対処法を知っておかないと思わぬボトムネックになりかねない。

それさえクリアすれば、
コントロール操作系の関数を安全にユニットテストする方法として適していると思う。

関連記事

comments powered by Disqus