2019/6/2追記
改訂版を投稿しました。
こちらをご覧ください。
前提
Microsoft Officeに付属するVBEは最も普及しているIDEといっていいのではないでしょうか。業務の現場では、簡単なExcelワークシートの編集から、Accessなどによる大規模なアプリまで、幅広く使用されていることと思います。
しかし、開発に使われる言語であるVBAは、絶望的に不便な言語です。モダンな言語がしのぎを削る現在において、ここまでプログラマを舐めきった言語はそうないでしょう。
しかし、私は仕事上どうしてもExcel VBA(並びにVBScript)での開発をやらねばなりません。VBAも一応はプログラミング言語。規模の大きなプロダクトを作る能力を持っています。そのようなプロダクトの開発においては、当然ながらきちんとテストを実施する必要があります。
本記事では、Excel VBAを題材に、VBAでテストを行なう手法について考察してみたいと思います。
この記事を開いてくださった方の中にも、私のような境遇にあるというプログラマがいらっしゃるかもしれません。そうした方々の一助となれば幸いです。
目次
- クラスモジュールの使用
- 副作用の隔離
- ファクトリ
- モック
- DI
- ユニットテスト
クラスモジュールの使用
目次を見て気づかれた方もおられると思いますが、本記事ではDI(依存性の注入)により機能をテストダブルに置き換える手法をとります。
サービスにあたる振る舞いのみのモジュールは、標準モジュールとして定義するのが普通かと思いますが、DIのためにサービスもクラスモジュールとして定義しましょう。
副作用の隔離
Excel VBAによるアプリは、Excelワークシートに対する読み書きが多く発生するので、副作用をきちんと管理しておかないとテスト可能性が低くなります。
副作用といっても、ダイアログの表示やファイル入出力等、色々ありますが、ここではExcelワークシートの単純な読み書きについて考えてみたいと思います。
まずは、Excelの読み書きの機能を取り決めたIExcelIO
インターフェイスを定義します。
Option Explicit
Public Function OpenBook(ByVal FilePath As String) As Workbook
End Function
Public Function GetSheet(ByVal Book As Workbook, ByVal SheetName As String) As Worksheet
End Function
Public Function ReadCell(ByVal Sheet As Worksheet, ByVal Row As Long, ByVal Col As Long) As Variant
End Function
Public Sub WriteCell(ByVal Sheet As Worksheet, ByVal Row As Long, ByVal Col As Long, ByVal Value As Variant)
End Sub
次に、Excelの読み書きを扱うサービスとして、ExcelIO
クラスを定義します。上記のIExcelIO
インターフェイスを実装させます。
Option Explicit
Implements IExcelIO
Public Function IExcelIO_OpenBook(ByVal FilePath As String) As Workbook
Set IExcelIO_OpenBook = Application.Workbooks.Open(FilePath)
End Function
Public Function IExcelIO_GetSheet(ByVal Book As Workbook, ByVal SheetName As String) As Worksheet
Dim Sheet As Object
For Each Sheet In Book.Worksheets
If Sheet.Name = SheetName Then
Set IExcelIO_GetSheet = Sheet
Exit Function
End If
Next
Set IExcelIO_GetSheet = Nothing
End Function
Public Function IExcelIO_ReadCell(ByVal Sheet As Worksheet, ByVal Row As Long, ByVal Col As Long) As Variant
IExcelIO_ReadCell = Sheet.Cells(Row, Col).Value
End Function
Public Sub IExcelIO_WriteCell(ByVal Sheet As Worksheet, ByVal Row As Long, ByVal Col As Long, ByVal Value As Variant)
Sheet.Cells(Row, Col).Value = Value
End Sub
このExcelIO
クラスは、Excelワークシートを読み書きする上で最低限の機能をラッピングしただけのものです。しかし、単純な読み書きのみのアプリであれば、ここに隔離した副作用で事足ります。
こうして隔離しておけば、テストの際にこのクラスをテストダブルに置き換えてしまうことで、単体テストの環境が整います。
ファクトリ
DIの対象となるのはサービスですが、実際の開発では複数のサービスクラスが存在するでしょう。管理を楽にするために、それらを生成するファクトリを用意しておきましょう。
Option Explicit
Public Constants as IConstants
Public ExcelIO As IExcelIO
Public XXXService as IXXXService
Private Sub Class_Initialize()
Set Constants = New Constants
Set ExcelIO = New ExcelIO
Set XXXService = New XXXService
Call XXXService.Init(Constants)
End Sub
モック
さて、ここで以下のようなクラスをテストしたいとしましょう。
- (1, 1)セルと(2, 1)セルの値を読み込む
- (3, 1)セルに両者を足し合わせた値を書き込む
これをテストするためのモックを用意します。
Option Explicit
Implements IExcelIO
Public TargetFilePath As String
Public TargetSheetName As String
Public TargetRow As Long
Public TargetCol As Long
Public WrittenValue As Variant
Public Function IExcelIO_OpenBook(ByVal FilePath As String) As Workbook
TargetFilePath = FilePath
Set IExcelIO_OpenBook = Nothing
End Function
Public Function IExcelIO_GetSheet(ByVal Book As Workbook, ByVal SheetName As String) As Worksheet
TargetSheetName = SheetName
Set IExcelIO_GetSheet = Nothing
End Function
Public Function IExcelIO_ReadCell(ByVal Sheet As Worksheet, ByVal Row As Long, ByVal Col As Long) As Variant
IExcelIO_ReadCell = Row + Col
End Function
Public Sub IExcelIO_WriteCell(ByVal Sheet As Worksheet, ByVal Row As Long, ByVal Col As Long, ByVal Value As Variant)
TargetRow = Row
TargetCol = Col
WrittenValue = Value
End Sub
場合によっては、ファクトリも用意しましょう(今回は使用しません)。
Option Explicit
Public Constants as IConstants
Public ExcelIO As IExcelIO
Public XXXService as IXXXService
Private Sub Class_Initialize()
Set Constants = New Mock_Constants
Set ExcelIO = New Mock_ExcelIO
Set XXXService = New Mock_XXXService
Call XXXService.Init(Constants)
End Sub
DI
DI(依存性の注入)は、具象型を外部で生成しておき、それを対象クラスの抽象型フィールドに注入することで、型に関する依存性を解決する手法です。今回は、初期化メソッドの引数に注入する方法をとります。
前節で示したテスト対象は、以下のようなものでした。
- (1, 1)セルと(2, 1)セルの値を読み込む
- (3, 1)セルに両者を足し合わせた値を書き込む
このクラスに、Init
メソッドを通じてサービスを注入します。以下にコードを示します。
Option Explicit
Private ExcelIO_ As IExcelIO
Public Sub Init(ByVal ExcelIO As IExcelIO)
Set ExcelIO_ = ExcelIO
End Sub
Public Sub SampleMethod()
Dim Book As Workbook: Set Book = ExcelIO_.OpenBook("C:\Users\UserName\Desktop\Test.xlsx")
Dim Sheet As Worksheet: Set Sheet = ExcelIO_.GetSheet(Book, "Sheet1")
Dim Value1 As Long: Value1 = ExcelIO_.ReadCell(Sheet, 1, 1)
Dim Value2 As Long: Value2 = ExcelIO_.ReadCell(Sheet, 2, 1)
Call ExcelIO_.WriteCell(Sheet, 3, 1, Value1 + Value2)
End Sub
ユニットテスト
テストにおいては、SampleClass
に注入されるのはMock_ExcelIO
クラスです。
テストはクラスである必要がないので、標準モジュールに定義します。
Option Explicit
Public Sub Test_SampleMethod()
Dim Mock_ExcelIO As Mock_ExcelIO: Set Mock_ExcelIO = New Mock_ExcelIO
Dim SampleClass As SampleClass: Set SampleClass = New SampleClass
Call SampleClass.Init(Mock_ExcelIO)
Call SampleClass.SampleMethod
Debug.Assert "C:\Users\UserName\Desktop\Test.xlsx" = Mock_ExcelIO.TargetFilePath
Debug.Assert "Sheet1" = Mock_ExcelIO.TargetSheetName
Debug.Assert 3 = Mock_ExcelIO.TargetRow
Debug.Assert 1 = Mock_ExcelIO.TargetCol
Debug.Assert 5 = Mock_ExcelIO.WrittenValue
End Sub
実行ボタンを押してエラー停止しなければ、テストが通っています。
以上により、テストダブルを用いたユニットテストを正常に行なうことができました。
まとめ
- テストコード以外はクラスモジュールとして定義する。
- 副作用を特定のサービスクラスに隔離する。
- モックを作成し、DIで注入する。
- テストはツライが楽しく行なう。
以上です。ありがとうございました。