VBAにはユニットテストやリファクタリング機能がない・・・そんなふうに考えていた時期が俺にもありました

# はじめに
ワルいけどVBAは開発環境としてあまりにも不完全すぎる。

VBAにはユニットテストがない
リファクタリング機能がない
静的解析がない
以上の理由でVBAは開発環境として不完全だッ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/59578683-d27b-5ae1-ff67-0b43d6c7e713.png)

※実際のところ、VBAUnitとかコードをエクスポートして静的解析とかできますが、今回はパス。

## 殺伐とした開発環境に救世主が!!!
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/97a2b2ec-5b00-a3b8-dedc-e131f5f828c5.png)

RubberduckはVBA6,VBA7 x86/x64,VB6の開発環境であるVBEを拡張するものです。

オープンソースで開発されており、以下のページからダウンロードが可能です。
https://github.com/rubberduck-vba/Rubberduck

上記からインストールすることで、VBEはRubberduckの機能を含んだものに拡張されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/1a5f1da8-18f6-ad3e-e632-807958b118f3.png)

※メニューとツールバーが追加されている

## Rubberduckの機能
### コードメトリックスを表示する
Rubberduck->Tools->Code Metricesを選択することで行数、ネスト数、循環的複雑度を表示できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/6a3244b8-7cb7-baee-f099-0abef220f3b7.png)


もし表示されている情報が古い場合は以下の方法で更新がおこなえます。
・Rubberduck->Refresh　を選択する
・ツールバーの![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/ad864ee9-055d-1732-6679-fa71484cd3b5.png)を押下する。

### Code Inspections
Rubberduck->Code Inspectionsでコードの静的解析の結果が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/17c50cd3-3c03-64fa-959e-73666916adf6.png)

たとえば「Option Explicit」を宣言していない場合や、使っていない変数があるとその旨を表示してくれます。

Fixメニューを使用すると修正または警告を無視できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/48518cbf-28c8-12c6-0aa5-31a23882219b.png)

### 参照箇所
エディタ上で関数を選択すると、Rubberduckのツールバーにその関数を参照している箇所の数が表示されます。それをクリックすることで参照している場所を確認できます。

![vba002.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/20086887-8a82-2cfc-ccab-a8dddcd92cb7.gif)

### リファクタリング機能
コンテキストメニューまたは、メニューバーのRubberduckメニューからRefactorを実行することでリファクタリングが行えます。
下記の例では関数名を変更しています。

![vba003.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/585c8d4c-7446-2c02-1ff7-11ce561f4f05.gif)

他にも引数の削除や順番が行えます。

### ユニットテスト
#### テスト用のモジュールとメソッドの追加方法
Rubberduck→Unit Tests→Test Moduleでテストモジュールを追加、Rubberduck→Unit Tests→Test Methodでテストメソッドを追加します。

![vba004.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/5882b457-1c84-906e-8d1f-1ec0fcdc358f.gif)

テストモジュール中の「'@TestMethod」というコメントが記載された関数がテストメソッドとなります。

#### テスト実行例
まず以下のようなテストモジュールを用意します。

```vb

Option Explicit
Option Private Module

'@TestModule
'@Folder("Tests")

Private Assert As Object
Private Fakes As Object

'@ModuleInitialize
Private Sub ModuleInitialize()
    'this method runs once per module.
    Set Assert = CreateObject("Rubberduck.AssertClass")
    Set Fakes = CreateObject("Rubberduck.FakesProvider")
End Sub

'@ModuleCleanup
Private Sub ModuleCleanup()
    'this method runs once per module.
    Set Assert = Nothing
    Set Fakes = Nothing
End Sub

'@TestInitialize
Private Sub TestInitialize()
    'this method runs before every test in the module.
End Sub

'@TestCleanup
Private Sub TestCleanup()
    'this method runs after every test in the module.
End Sub

'@TestMethod("足し算の検証")
Private Sub TestMethodOK()
    On Error GoTo TestFail
    
    'Arrange:

    'Act:
    Dim act As Long
    act = 5 + 3
    
    'Assert:
    Assert.AreEqual 8&, act

TestExit:
    Exit Sub
TestFail:
    Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub

'@TestMethod("足し算の検証")
Private Sub TestMethodNG()
    On Error GoTo TestFail
    
    'Arrange:

    'Act:
    Dim act As Long
    act = 5 + 3
    
    'Assert:
    Assert.AreEqual 7&, act

TestExit:
    Exit Sub
TestFail:
    Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub

```

なおAssertクラスがサポートしている機能は以下の通りです。
https://github.com/rubberduck-vba/Rubberduck/wiki/Unit-Testing#the-assert-class

テストコードを記載したらTestExplorerからテストを実行します。もし、テストコードが反映されていない場合は![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/72905ebd-2689-0f4a-d740-5e0272db04fa.png)で更新ができます。

![vba005.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/c9d27d6b-11eb-641b-d123-a6ba1f9fbb4d.gif)

現時点でUIなしで実行はできないようです。

**VB6 should offer a command line option for UI-less operation. #4099**
https://github.com/rubberduck-vba/Rubberduck/issues/4099

#### Fakeの利用
現在時刻などをFakesで偽装してテストに都合のいい値を返すことができます。
たとえば以下のようなテストしずらいコードがあったとします。

```vb:テスト対象のコード
Option Explicit

Public Function hoge1() As Long
    Dim ret As String
    ret = InputBox("Input Number")
    hoge1 = Val(ret) * 2
    Debug.Print hoge1
End Function

Public Function hoge2() As String
    hoge2 = Format(Now(), "yyyy/mm/ddです")
End Function

```

Fakesを利用することでInputBoxやNowをテスト用に偽装することができます。

```vb

Option Explicit
Option Private Module

'@TestModule
'@Folder("Tests")

Private Assert As Object
Private Fakes As Object

'@ModuleInitialize
Private Sub ModuleInitialize()
    'this method runs once per module.
    Set Assert = CreateObject("Rubberduck.AssertClass")
    Set Fakes = CreateObject("Rubberduck.FakesProvider")
End Sub

'@ModuleCleanup
Private Sub ModuleCleanup()
    'this method runs once per module.
    Set Assert = Nothing
    Set Fakes = Nothing
End Sub

'@TestInitialize
Private Sub TestInitialize()
    'this method runs before every test in the module.
End Sub

'@TestCleanup
Private Sub TestCleanup()
    'this method runs after every test in the module.
End Sub
'@TestMethod
Public Sub InputBoxFakeWorks()
    ' InputBoxを偽装して常に23という文字列を返す
    On Error GoTo TestFail
    Dim lRet As Long
    Fakes.InputBox.Returns "23"
    
    lRet = Module1.hoge1()
    
    Assert.AreEqual 46&, lRet
    Fakes.InputBox.Verify.Once
    Fakes.InputBox.Verify.Parameter "Prompt", "Input Number"
     
TestExit:
    Exit Sub
TestFail:
    Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub

'@TestMethod
Public Sub NowFakeWorks()
    ' Now()を偽造して現在時刻を偽物を返却
    On Error GoTo TestFail
    Dim sRet As String
    Fakes.Now.Returns #2/3/1999 5:32:32 AM#
    
    sRet = Module1.hoge2()
    
    Assert.AreEqual "1999/02/03です", sRet
     
TestExit:
    Exit Sub
TestFail:
    Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
```

[ロジック](https://github.com/rubberduck-vba/Rubberduck/blob/86b22cb863c0b8b59f3e442cda261d70129824b6/Rubberduck.Main/ComClientLibrary/UnitTesting/FakesProvider.cs
)を見る限りFakeできる関数は以下のものだけのようです。

 - MsgBox
 - InputBox
 - Beep
 - Environ
 - Timer
 - DoEvents
 - Shell
 - SendKeys
 - Kill
 - MkDir
 - RmDir
 - ChDir
 - ChDrive
 - CurDir
 - Now
 - Time
 - Date

[こんなかんじ](https://github.com/rubberduck-vba/Rubberduck/blob/86b22cb863c0b8b59f3e442cda261d70129824b6/Rubberduck.Main/ComClientLibrary/UnitTesting/Fakes/Now.cs)でVBAの関数のDLLインジェクションしているっぽい。

#### モックの利用
以下のプルリクエストでモックフレームワークの導入されるようになるらしい。
https://github.com/rubberduck-vba/Rubberduck/pull/4681

## まとめ
このように想像以上に色々できます。
また、ソースコードが公開されているので、自分でなんとかすることもできます。
VBAで開発する場合は入れてみると便利ではないでしょうか。