はじめに
本記事は、1年前に書いた以下の記事を大幅に見直して書き改めたものです。
上記の記事は、私の古い認識のもとに書き上げられました。
本記事では、もうちょっと成長した私のテスト手法を見ていただけるかと思います。
対象とするアプリケーション
指定したセルから最終行まで、各セルの値の2倍を計算し、右隣のセルに書き出します。
以下の値が…
こうなります。
プロジェクト構成
-
MainModule
モジュール- プログラムのエントリポイントです。
-
AnswerWriter
クラス- 処理の本体です。セルの値を読み、計算し、結果を書き出します。
-
IEffect
インターフェイス- 下記
Effect
クラスのインターフェイスです。
- 下記
-
Effect
クラス- 副作用をまとめたクラスです。
-
EffectMock
クラス- 上記
Effect
クラスのモックです。
- 上記
-
MockUtil
モジュール- モックを扱うための機能を持つモジュールです。
-
G
モジュール- Gは'Global'のGです。Globalが予約語であることと、使用回数が多いので利便性のためにこうしています。
-
Test_AnswerWriter
モジュール- 上記
AnswerWriter
クラスのテストです。
- 上記
実際のコード
まずは、処理本体である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
を定義するのみです。
Option Explicit
Public Effect As IEffect
Effect
変数はIEffect
インターフェイス型になっています。
これは、後ほどモックと差し替えるためです。
IEffect
には、3つのメソッドが定義されています。
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
クラスの実装を以下に示します。
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
これで、機能の定義は全てです。
最後に、プログラムのエントリポイントを実装します。
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
をクリックします。
以上でDictionary
を使用できます。
では、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
を以下に示します。
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
これでテストが書けます。
テストコードを以下に示します。
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
をうまく使えば、モックのような何かが作れます。
モックを定義するのは、しんどかったり、そもそも無理だったりするケースもありますが、本記事のやり方は意外と使える場面は多いと思います。
楽しく有効なテストを心がけましょう。
以上です。
ありがとうございました。