【今さら極める VB6 -Part 2】VB6 でもユニットテスト／モック利用

VB6 でも（簡易的・擬似的ではありますが）ユニットテスト[^1]を実装することができます。
モックオブジェクトを利用することも可能です。

依存コンポーネント (Depend-On Component）として、[Part 1](https://qiita.com/CodeOne/items/36aa1c59e5e83c9a5e9b) にコードを掲載した VB6 版 `StringBuilder` クラスを使用しますので、初めにそちらをご覧ください。
ほかに特別なサードパーティコンポーネントは使いません。

# 手法と技術的背景

## ユニットテスト

以下の手順でユニットテストを実装・実行できます。

1. テスト用の標準モジュールを作成します。
2. テスト用の `Public` メソッドを定義します。
3. テストメソッド内に対象となる処理を記述します。
4. `Debug.Assert` で処理結果を検証します。
5. イミディエイトウィンドウからテストメソッドを実行します。テストメソッド呼び出しを集めたメソッドを作成しておくと一括実行できます。

## Implements ステートメントと疑似的な継承

VB6 には `Implements` という、インターフェイスの実装をするためのステートメントが用意されています。
.NET や Java の「インターフェイス」とは以下のような点で異なる、簡易的なゆるい仕組みです。

- インターフェイスとして指定されるのはただのクラスである。
- 実装メソッドは {InterfaceClass}_{Method} という、イベントハンドラのような形式の名称で定義する。
- 実装メソッドは既定では `Private` のため、実装クラス型の変数から実装メソッドに直接アクセスすることはできない（`Public` に変えたり、`Public` メソッドを通してアクセスすることはできる）。

この `Implements` ステートメントのゆるい点をポジティブに活用すると、次のような方法で擬似的な継承を実現することができます。

1. インターフェイスとして使用するクラスを基底クラスと位置づけ、共通ロジックを記述する。
2. 実装クラスのメンバ変数（フィールド）で、インターフェイスとして使用するクラスのインスタンスを保持する。
3. 実装メソッドでは、必要に応じて「2」のインスタンスから本来のメソッドを呼び出しつつ、処理を上書きする。

# テストコード

以下は、この方法で作成したモッククラスを利用したユニットテストの例です。
[Part 1](https://qiita.com/CodeOne/items/36aa1c59e5e83c9a5e9b) の `StringBuilder` クラスを使用します。

## 依存コンポーネント (Depend-On Component）のテスト

まず `StringBuilderTest` という標準モジュールを作成して、依存コンポーネント `StringBuilder` のテストを記述します。

```vb:StringBuilderTest.bas
Option Explicit
 
'Length_SingleLine
Public Sub TestStringBuilder_Length_SingleLine()
     
    With New StringBuilder
        Debug.Assert .Length = 0
         
        Call .Append("")
        Debug.Assert .Length = 0
         
        Call .Append("あい")
        Debug.Assert .Length = 2
    End With
     
End Sub
 
'Length_MultiLine
Public Sub TestStringBuilder_Length_MultiLine()
     
    With New StringBuilder
        .LineSeparator = adLF
         
        Call .AppendLine("Line")
        Debug.Assert .Length = 5
    End With
     
End Sub
 
'ToString
Public Sub TestStringBuilder_ToString()
     
    With New StringBuilder
        .LineSeparator = adLF
         
        Call .Append("Code")
        Debug.Assert .ToString() = "Code"
         
        Call .AppendLine("One")
        Debug.Assert .ToString() = "CodeOne" & vbLf
    End With
     
End Sub
```

#### テスト実行

テストメソッドはイミディエイトウィンドウから実行します。
成功すればそのままカーソルが下の行に移動します。
検証に失敗すると、失敗した位置で実行が止まります。

## 依存コンポーネント (Depend-On Component）のモッククラス

次に `StringBuilder` のモッククラス `MockStringBuilder` を作成し、ユニットテスト向けに処理を上書きします。
ここでは `Append`, `AppendLine` に渡された引数値を保持し、`ToString()` では `ToStringReturn` で渡された文字列を返すようにします。 
`LineSeparator` プロパティで実装しているように、`base` フィールドを使用して Moq の `CallBase` 的に本来の処理を呼び出すことも可能です。

```vb:MockStringBuilder.cls
Option Explicit

'インターフェイス
'　StringBuilder 型で扱えるよう、同じインターフェイスを StringBuilder_{Method} 形式で実装する。
Implements StringBuilder

'委譲インスタンス
'　擬似的な継承を実現するため、インスタンスを保持する。
Private base As New StringBuilder

'モック用のフィールド
Private mAppendCalledArgs As New Collection
Private mAppendLineCalledArgs As New Collection
Private mToStringReturn As String

'Append に渡された引数配列のコレクション
Public Property Get AppendCalledArgs() As Collection
    Set AppendCalledArgs = mAppendCalledArgs
End Property

'AppendLine に渡された引数配列のコレクション
Public Property Get AppendLineCalledArgs() As Collection
    Set AppendLineCalledArgs = mAppendLineCalledArgs
End Property

'ToString で返す値
Public Property Get ToStringReturn() As String
    ToStringReturn = mToStringReturn
End Property
Public Property Let ToStringReturn(ByVal s As String)
    mToStringReturn = s
End Property

'改行文字
Private Property Let StringBuilder_LineSeparator(ByVal e As LineSeparatorsEnum)
    base.LineSeparator = e
End Property
Private Property Get StringBuilder_LineSeparator() As LineSeparatorsEnum
    StringBuilder_LineSeparator = base.LineSeparator
End Property

'文字列の長さ
Private Property Get StringBuilder_Length() As Long
    Debug.Assert False
End Property

'文字列を追加する。
Private Function StringBuilder_Append(ByVal s As String) As StringBuilder
    
    '渡された引数を保持する。
    Call mAppendCalledArgs.Add(Array(s))
    
End Function

'改行付で文字列を追加する。
Private Function StringBuilder_AppendLine(Optional ByVal s As String = "") As StringBuilder
    
    '渡された引数を保持する。
    Call mAppendLineCalledArgs.Add(Array(s))
    
End Function

'文字列化する。
Private Function StringBuilder_ToString() As String
    
    'モック用の値を返す。
    StringBuilder_ToString = mToStringReturn
    
End Function

```

## テスト対象システム（System Under Test）

`StringBuilder` を使用するクラス `Sample` です。
テキストを返す `GetText` メソッドが定義されています。

```vb:Sample.cls
Option Explicit
 
Private mBuilder As StringBuilder
Public Property Get Builder() As StringBuilder
    Builder = mBuilder
End Property
Public Property Set Builder(ByVal o As StringBuilder)
    Set mBuilder = o
End Property
 
Public Sub Class_Initialize()
     
    Set mBuilder = New StringBuilder
     
End Sub
 
Public Function GetText() As String
     
    Call mBuilder.Append("Sample Calls StringBuilder")
    GetText = mBuilder.ToString()
     
End Function
```

## テスト対象システム（System Under Test）のユニットテスト

`Sample` クラスの `GetText` メソッドをテストするのに `MockStringBuilder` を使用します。
`GetText()` を呼ぶと、本来の `StringBuilder` に変わって `MockStringBuilder` オブジェクトがテキストを処理します。

```vb:SampleTest.bas
Option Explicit
 
'GetText
Public Sub TestSample_GetText()
    
    'モックオブジェクトに戻り値を設定
    Dim oMockBuilder As New MockStringBuilder
    oMockBuilder.ToStringReturn = "MockStringBuilder Returns"
    
    With New Sample
        Set .Builder = oMockBuilder
        
        '対象メソッド実行
        Dim sActualText As String
        sActualText = .GetText()
        
        '1回目の Append 呼び出しの第1引数を検証
        Debug.Assert oMockBuilder.AppendCalledArgs(1)(0) = "Sample Calls StringBuilder"
        
        'AppendLine は呼び出されていないことを検証
        Debug.Assert oMockBuilder.AppendLineCalledArgs.Count = 0
        
        '戻り値を検証
        Debug.Assert sActualText = "MockStringBuilder Returns"
    End With
    
End Sub
```

#### テスト実行

イミディエイトウィンドウからテストコードを実行します。

```
TestSample_GetText
```

通りましたね。
テストメソッドが増えたら、呼び出しを1つのメソッドにまとめて一括実行しましょう。

以上です。

[^1]: VB6 では自動テストが一般化しておらず、「単体テスト」というと「結合テスト」との対比で機能／コンポーネント単位の手動テストを指すことがあるため、ここではあえて日本の VB6 界であまり使われない「ユニットテスト」という表現を用います。