[Access VBA] フォームのコントロール操作系の関数をどのようにユニットテストするかの方法メモ
概要
この記事について
かんたんな概要と結論
実際に使用するフォームなどの環境から切り離して
コントロール操作系の関数のユニットテストができる。
MSAccessのVBAで安定性のあるアプリケーションを作成する場合、
関数を機能単位で分割してユニットテストすると安定性が高まる。
純粋な関数の場合はテストは簡単だが、
関数の外部のグローバル変数やコンポーネント
(フォームのテキストボックスの値や背景色など)を変更する場合はユニットテストが難しくなり、
また、使用するフォームやクエリに何かしらの影響を与えて、それらを汚す(想定外の挙動を付与する)かもしれない。
フォームのコントロール操作系の関数にしぼって考えると、
VBAによってフォームやその上のコントロールを動的に生成し、
後始末をちゃんとすることで、
コード上でテスト環境の作成まですべて完結、かつ環境をなるべく汚さない方法でユニットテストできるだろう。
その方法を記したい。
説明のために作成したサンプルを含むツール(Accessファイル)とソースコードはこちらでダウンロードできます。
作成環境
- windows10
- MSOffice2019
想定するケース
About
次のように、
- ボタン内容でプルダウンのリストを切り替える関数
- ボタン内容で入力欄の使用可能/不可能を切り替える関数
の機能をそれぞれもつフレームがある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
Name
やRowSourceType
の指定は
デザインビューでないと機能しない
(本当は何か回避策があるかもしれないが、私のコードだとそうなった)ため、
このようにしてデザインビューにおいて開閉することでプロパティをテスタブルに設定した。
同様に、
次のようにコンボボックスのリスト項目を変更して参照する場合も
フォームビューで開いておかないとエラーとなるため、
次のようにする。
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
メソッドで評価。
もし想定通りならば実行は一時停止せずにそのまま処理される。
最後にフォームが削除され、
環境を汚さずにテストが終了する。
まとめ
コントロールを動的に設定する際に
いくつか気をつけないとエラーを吐くのは
対処法を知っておかないと思わぬボトムネックになりかねない。
それさえクリアすれば、
コントロール操作系の関数を安全にユニットテストする方法として適していると思う。
関連記事
- [Access VBA] RequeryとRefreshの使い分けデモ 「単票フォームの編集」編
- [Access VBA] 見積書作成ツール(Accessバージョン)を作成した
- [VBA, PowerShell] Accessのモジュール・クラスやクエリのSQLから特定文字列を抽出するためのテクニック
- [VBA] クラスを利用するメリットと方法について & 簡単なサンプル(2)
- [Access VBA] サブフォーム上のレコードを挿入、削除する簡単なサンプル