【改訂版】VBAだってユニットテストがしたい！

# はじめに

本記事は、1年前に書いた以下の記事を大幅に見直して書き改めたものです。

[VBAだってユニットテストがしたい！](https://qiita.com/hiki_neet_p/items/a0ce31afe12ed912fec8)

上記の記事は、私の古い認識のもとに書き上げられました。
本記事では、もうちょっと成長した私のテスト手法を見ていただけるかと思います。

# 対象とするアプリケーション

指定したセルから最終行まで、各セルの値の2倍を計算し、右隣のセルに書き出します。

以下の値が…

![initial-table.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53668/eaf0b7a9-7ad6-f2de-36dd-32cb170f1932.jpeg)

こうなります。

![processed-table.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53668/9bdfec01-05cc-05f3-c1a3-9ea6410a3660.jpeg)

# プロジェクト構成

![project-structure.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53668/ab23fc13-f6d0-414b-4163-83f536cab9c4.jpeg)

- `MainModule`モジュール
  - プログラムのエントリポイントです。

- `AnswerWriter`クラス
  - 処理の本体です。セルの値を読み、計算し、結果を書き出します。

- `IEffect`インターフェイス
  - 下記`Effect`クラスのインターフェイスです。

- `Effect`クラス
  - 副作用をまとめたクラスです。

- `EffectMock`クラス
  - 上記`Effect`クラスのモックです。

- `MockUtil`モジュール
  - モックを扱うための機能を持つモジュールです。

- `G`モジュール
  - Gは'Global'のGです。Globalが予約語であることと、使用回数が多いので利便性のためにこうしています。

- `Test_AnswerWriter`モジュール
  - 上記`AnswerWriter`クラスのテストです。

# 実際のコード

まずは、処理本体である`AnswerWriter`クラスです。

```vb:AnswerWriter
Option Explicit

Public Sub WriteAnswer(ByVal Sheet As Worksheet, ByVal Row As Long, ByVal Col As Long)
    
    '最終行を取得
    Dim EndRow As Long: EndRow = G.Effect.GetEndRow(Sheet, Col)
    
    'セル範囲を取得
    Dim Matrix As Variant: Matrix = G.Effect.ReadMatrix(Sheet, Row, Col, EndRow, Col)
    
    Dim I As Long
    Dim J As Long: J = LBound(Matrix, 2)
    
    '各値の2倍を計算
    For I = LBound(Matrix, 1) To UBound(Matrix, 1)
        Matrix(I, J) = Matrix(I, J) * 2
    Next
    
    'セル範囲を書き出す
    Call G.Effect.WriteMatrix(Sheet, Row, Col + 1, EndRow, Col + 1, Matrix)
    
End Sub
```

上記コードでは、3か所で`G.Effect`を使用しています。
これは、副作用を持つ機能を定義した`Effect`クラスのインスタンスです。
`G`モジュールはグローバル変数`Effect`を定義するのみです。

```vb:G
Option Explicit

Public Effect As IEffect
```

`Effect`変数は`IEffect`インターフェイス型になっています。
これは、後ほどモックと差し替えるためです。
`IEffect`には、3つのメソッドが定義されています。

```vb:IEffect
Option Explicit

'指定された列の最終行を取得
Public Function GetEndRow(ByVal Sheet As Worksheet, ByVal Col As Long) As Long
End Function

'指定された範囲の値を2次元配列で取得
Public Function ReadMatrix( _
        ByVal Sheet As Worksheet, _
        ByVal Row1 As Long, _
        ByVal Col1 As Long, _
        ByVal Row2 As Long, _
        ByVal Col2 As Long) As Variant
End Function

'指定された範囲に2次元配列の値を書き込む
Public Sub WriteMatrix( _
        ByVal Sheet As Worksheet, _
        ByVal Row1 As Long, _
        ByVal Col1 As Long, _
        ByVal Row2 As Long, _
        ByVal Col2 As Long, _
        ByVal Matrix As Variant)
End Sub
```

この実体である`Effect`クラスの実装を以下に示します。

```vb:Effect
Option Explicit

Implements IEffect

'指定された範囲の値を2次元配列で取得
Public Function IEffect_GetEndRow(ByVal Sheet As Worksheet, ByVal Col As Long) As Long
    
    IEffect_GetEndRow = Sheet.Cells(Sheet.Rows.Count, Col).End(xlUp).Row
    
End Function

'指定された範囲の値を2次元配列で取得
Public Function IEffect_ReadMatrix( _
        ByVal Sheet As Worksheet, _
        ByVal Row1 As Long, _
        ByVal Col1 As Long, _
        ByVal Row2 As Long, _
        ByVal Col2 As Long) As Variant
    
    IEffect_ReadMatrix = Sheet.Range(Sheet.Cells(Row1, Col1), Sheet.Cells(Row2, Col2)).Value
    
End Function

'指定された範囲に2次元配列の値を書き込む
Public Sub IEffect_WriteMatrix( _
        ByVal Sheet As Worksheet, _
        ByVal Row1 As Long, _
        ByVal Col1 As Long, _
        ByVal Row2 As Long, _
        ByVal Col2 As Long, _
        ByVal Matrix As Variant)
    
    Sheet.Range(Sheet.Cells(Row1, Col1), Sheet.Cells(Row2, Col2)).Value = Matrix
    
End Sub
```

これで、機能の定義は全てです。
最後に、プログラムのエントリポイントを実装します。

```vb:MainModule
Option Explicit

Public Sub Main()
    
    'グローバル変数にEffectインスタンスを設定
    Set G.Effect = New Effect
    
    '処理実行
    Dim AnswerWriter As AnswerWriter: Set AnswerWriter = New AnswerWriter
    Call AnswerWriter.WriteAnswer(Sheet1, 2, 2)
    
End Sub
```

以上で、プログラムは動作します。

# ユニットテスト

ユニットテストの対象は`AnswerWriter`クラスです。
しかし、副作用が邪魔です。
副作用を隔離してある`Effect`クラスをモックに差し替えます。

モックを作成するには、`Dictionary`クラスが必要です。
**ツール -> 参照設定**から、`Microsoft Scripting Runtime`をチェックし、`OK`をクリックします。

![sanshou-settei.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53668/bc29a868-b7c9-bc37-c666-0a13c138804e.jpeg)

以上で`Dictionary`を使用できます。

では、`EffectMock`クラスを以下に示します。

```vb:EffectMock
Option Explicit

Implements IEffect

Public GetEndRow_Values As Dictionary    'GetEndRowの戻り値を設定（スタブ）
Public ReadMatrix_Values As Dictionary   'ReadMatrixの戻り値を設定（スタブ）
Public WriteMatrix_Results As Dictionary 'WriteMatrixの引数を記録（スパイ）

Private Sub Class_Initialize()
    
    Set GetEndRow_Values = New Dictionary
    Set ReadMatrix_Values = New Dictionary
    Set WriteMatrix_Results = New Dictionary
    
End Sub

Public Function IEffect_GetEndRow(ByVal Sheet As Worksheet, ByVal Col As Long) As Long
    
    '事前に設定された戻り値を返す
    IEffect_GetEndRow = MockUtil.GetValue(GetEndRow_Values, Col)
    
End Function

Public Function IEffect_ReadMatrix( _
        ByVal Sheet As Worksheet, _
        ByVal Row1 As Long, _
        ByVal Col1 As Long, _
        ByVal Row2 As Long, _
        ByVal Col2 As Long) As Variant
    
    '事前に設定された戻り値を返す
    IEffect_ReadMatrix = MockUtil.GetValue(ReadMatrix_Values, Row1, Col1, Row2, Col2)
    
End Function

Public Sub IEffect_WriteMatrix( _
        ByVal Sheet As Worksheet, _
        ByVal Row1 As Long, _
        ByVal Col1 As Long, _
        ByVal Row2 As Long, _
        ByVal Col2 As Long, _
        ByVal Matrix As Variant)
    
    '引数を記録
    Call MockUtil.SetValue(WriteMatrix_Results, Matrix, Row1, Col1, Row2, Col2)
    
End Sub
```

このモックで使用している`MockUtil`ですが、`Dictionary`に対し、引数と戻り値の組み合わせを保存したり、参照したりするための機能を有します。
`MockUtil`を以下に示します。

```vb:MockUtil
Option Explicit

'指定された引数群から戻り値を取得
Public Function GetValue(ByVal Values As Dictionary, ParamArray Args() As Variant) As Variant
    
    If IsObject(Values(GetKey(Args))) Then
        Set GetValue = Values(GetKey(Args))
    Else
        GetValue = Values(GetKey(Args))
    End If
    
End Function

'指定された引数群に対する戻り値を保存
Public Sub SetValue(ByVal Values As Dictionary, ByRef Value As Variant, ParamArray Args() As Variant)
    
    If IsObject(Value) Then
        Set Values.Item(GetKey(Args)) = Value
    Else
        Values.Item(GetKey(Args)) = Value
    End If
    
End Sub

'引数群に対応するDictionaryのキーを取得
Private Function GetKey(ByVal Args As Variant) As String
    
    GetKey = Join(Args, "|")
    
End Function
```

これでテストが書けます。
テストコードを以下に示します。

```vb:Test_AnswerWriter
Option Explicit

Public Sub Test()
    
    '行と列は以下の値を使用
    Dim Row As Long: Row = 2
    Dim Col As Long: Col = 2
    Dim EndRow As Long: EndRow = 5
    
    'ReadMatrixの戻り値を作成する
    Dim Matrix() As Variant
    ReDim Matrix(1 To EndRow - Row + 1, 1 To 1)
    
    Dim I As Long
    
    For I = LBound(Matrix, 1) To UBound(Matrix, 1)
        Matrix(I, 1) = I
    Next
    
    'モックをインスタンス化
    Dim Mock As EffectMock: Set Mock = New EffectMock
    Set G.Effect = Mock
    
    'モックのメソッドの戻り値を設定
    Call MockUtil.SetValue(Mock.GetEndRow_Values, EndRow, Col)
    Call MockUtil.SetValue(Mock.ReadMatrix_Values, Matrix, Row, Col, EndRow, Col)
    
    '処理実行
    Dim AnswerWriter As AnswerWriter: Set AnswerWriter = New AnswerWriter
    Call AnswerWriter.WriteAnswer(Nothing, Row, Col)
    
    'WriteMatrixの書き込み内容を取得
    Dim Result As Variant: Result = MockUtil.GetValue(Mock.WriteMatrix_Results, Row, Col + 1, EndRow, Col + 1)
    
    '結果のアサーション
    For I = LBound(Result, 1) To UBound(Result, 1)
        Debug.Assert Result(I, 1) = I * 2
    Next
    
End Sub
```

これで、`Test`メソッドを実行すればテスト完了です。

# 注意点

`MockUtil`の実装内容を見て気づいたかもしれませんが、引数を**'|'（パイプ）**で繋いだ文字列をキーにしています。
よって、以下の欠点があります。

- '|'（パイプ）が含まれる文字列が正常に判定されない可能性がある。
- 文字列に変換できる値しか検査できない。
- 型の検査ができない。

これらによく注意して使用する必要があります。

# まとめ

`Dictionary`をうまく使えば、モックのような何かが作れます。
モックを定義するのは、しんどかったり、そもそも無理だったりするケースもありますが、本記事のやり方は意外と使える場面は多いと思います。
楽しく有効なテストを心がけましょう。

以上です。
ありがとうございました。