前記事 ではVBAで整数型変数のエンディアンを変換する関数を作ってみました。しかし、最初の実装にはバグがあり、テスト駆動で作り直すことにしたのでした…


ところで、VBAでエンディアンを変換する処理自体に興味を持っている人はほとんどいないでしょう。それよりも、テスト駆動開発を知らない人にどんな雰囲気なのか知ってもらう方がいいでしょうね。


世の中には単体テストを自動化するVBAUnit(xUnitのVBAバージョンですね)なるものがあるらしいですが、ここではもっと地味に原始的な方法を使ってやろうと思います(その分お手軽です)。


原始的な方法といえばDebug.Assertが使えるのですが、これだと成功・失敗はわかるもののなぜ失敗したのか少々わかりにくいです。そこで専用のAssert関数を自作するところから始めます。



Sub AssertHex(Expected, Actual)

  If Expected <> Actual Then

    Debug.Print "**** 失敗 **** 期待値: " & Hex(Expected) & _

                      ", 実データ:" & Hex(Actual)

  Else

    Debug.Print "---- 成功 ----"

  End If

End Sub



AssertHex関数は引数で期待値と実データをもらって値を比較し一致しなければ失敗のメッセージをイミディエイトウィンドウに表示するだけのシンプルなものです。ちょっとしたテストなのでこれだけでも十分間に合います。


準備ができたら、いよいよReverseIntEndian関数を実装していくわけですが、テスト駆動開発ではいきなりReverseIntEndian関数を作ることはしません。最初にテストコードを書きます。最初のテストコードはこんなもんでいいでしょう。



Sub TestReverseEndian()

  AssertHex 0, ReverseIntEndian(0)

End Sub



この時点ではまだ、ReverseIntEndian関数は存在しないのでこのコードは動きません。次にコンパイルが通るようにReverseIntEndian関数を追加しますが、最初はわざとテストに失敗するようにでたらめな値を返させます。そうすることで、テストが期待通りに動いていることを確認します。


(今の時点ではまだAssertHex関数の動作確認すらできていません。失敗するはずのテストが成功すればそれはテストプログラムのバグです。)



Function ReverseIntEndian(ByVal Data As Integer) As Integer

  ReverseIntEndian = -1 ← 最初はわざとテストが失敗するようにする

End Function



TestReverseEndian関数を実行してテストを走らせると次のようなメッセージがイミディエイトウィンドウに現れます。



**** 失敗 **** 期待値: 0, 実データ:FFFF



これは、期待通りの動作です。次にコードを次のように変更します。



Function ReverseIntEndian(ByVal Data As Integer) As Integer

  ReverseIntEndian = 0 ← まずは最初のテストを成功させる

End Function



再びTestReverseEndian関数を実行して動作を確認します。イミディエイトウィンドウには次のようなメッセージが現れるはずです。



---- 成功 ----



これも期待通りの動作です。この辺からいよいよ本格的に実装していきます。


でも、まずはテストコードから…



Sub TestReverseEndian()

  AssertHex 0, ReverseIntEndian(0)

  AssertHex &H201, ReverseIntEndian(&H102)

End Sub



ReverseIntEndian関数はまだ固定で0を返しているだけの実装なので、当然このテストは失敗します。テストを実行して実際に失敗することを確認したら次に本実装に入っていきます。



Function ReverseIntEndian(ByVal Data As Integer) As Integer

  ReverseIntEndian = (Data And &HFF) * &H100

  ReverseIntEndian = ReverseIntEndian Or (Data / &H100)

End Function



とりあえず、テストは通りますが上の実装にはバグがあります。ReverseIntEndianが負値をうまく処理できないのです。



Sub TestReverseEndian()

  AssertHex 0, ReverseIntEndian(0)

  AssertHex &H201, ReverseIntEndian(&H102)

  AssertHex &H1F8F, ReverseIntEndian(&H8F1F) ← 負値渡してみる

End Sub



テストを追加して実行するとこんなメッセージが出力されます。



**** 失敗 **** 期待値: 1F8F, 実データ:FF8F



変換後の上位バイトがFFになってしまっています。負の値を割り算した結果は負の値なので Or 演算したときに、上位バイトがFFになってしまっているようです。DataをLongに変換してFFFFでマスクして正の値として割り算すればこの問題は回避できるでしょう。


このテストを通すために次のように実装を変更することにします。



Function ReverseIntEndian(ByVal Data As Integer) As Integer

  ReverseIntEndian = (Data And &HFF) * &H100

  ReverseIntEndian = ReverseIntEndian Or _

               ((CLng(Data) And &HFFFF&) / &H100&)

End Function



これでテストは通るようになります。さて、負の値の扱いに注意しなければならないことがわかりました。今度は変換後の結果が負値になるケース(最上位ビットが立つケース)を見るために、テストコードを追加します。



Sub TestReverseEndian()

  AssertHex 0, ReverseIntEndian(0)

  AssertHex &H201, ReverseIntEndian(&H102)

  AssertHex &H1F8F, ReverseIntEndian(&H8F1F)

  AssertHex &H8F12, ReverseIntEndian(&H128F) ← 変換後が負値のケース

End Sub



このテストを実行すると「オーバーフローしました」といって怒られます。


  ...

  ReverseIntEndian = (Data And &HFF) * &H100 ← オーバーフローする

  ...


Integer型のデータで&H8F * &H100を実行すると結果をInteger型に格納できないのでオーバーフローするといって怒られるのです…


どうやらすべての計算を一度Long型に変換してから最後にIntegerに戻すほうがよさそうです。そこで次のようにコードを変更しました。



Function ReverseIntEndian(ByVal Data As Integer) As Integer

  Dim Temp As Long

  Temp = (CLng(Data) And &HFF&) * &H100&

  Temp = Temp Or ((CLng(Data) And &HFFFF&) / &H100&)

  If &H8000& And Temp Then

    ReverseIntEndian = Temp Or &HFFFF0000

  Else

    ReverseIntEndian = Temp

  End If

End Function



Tempの計算結果をReverseIntEndianに代入するときは注意が必要です。そのまま代入したのではやはり「オーバーフローしました」と言って怒られます。計算結果が負値になるようにTemp Or &HFFFF0000で上位2バイトのビットを補ってから代入します。


しかし、テストを実行すると…



**** 失敗 **** 期待値: 8F12, 実データ:8F13



どうも、割り算の結果がおかしくなってます…


と、ここでようやくVBAの除算演算子には『/』と『\』があるのを思い出しました。/ 演算子は結果を浮動小数点数で返します。しかも、それを整数型の変数に代入すると勝手に四捨五入されてしまうのです(\ 演算子は結果を整数で返します)。これが原因でテストに失敗していることに気づき、最終的に次のようなコードにたどり着きました。



Function ReverseIntEndian(ByVal Data As Integer) As Integer

  Dim Temp As Long

  Temp = (CLng(Data) And &HFF&) * &H100&

  Temp = Temp Or ((CLng(Data) And &HFF00&) \ &H100&)

  If &H8000& And Temp Then

    ReverseIntEndian = Temp Or &HFFFF0000

  Else

    ReverseIntEndian = Temp

  End If

End Function



これで、テストは成功します。最後に念のためFFFFの変換も追加しておきます。しかし、テストは通るはずです。



Sub TestReverseEndian()

  AssertHex 0, ReverseIntEndian(0)

  AssertHex &H201, ReverseIntEndian(&H102)

  AssertHex &H1F8F, ReverseIntEndian(&H8F1F)

  AssertHex &H8F12, ReverseIntEndian(&H128F)

  AssertHex &HFFFF, ReverseIntEndian(&HFFFF) ← 念のため追加

End Sub



これを実行するとすべてのテストは成功します。これでReverseIntEndian関数の実装は完了です。4バイトの整数のエンディアンを変換するReverseLngEndian関数の実装も同様にテスト駆動で作りましたが、記事が長くなるのでこの辺で止めときます。



このように、テスト駆動開発では失敗と成功を交互に繰り返し、テストコードを蓄積しながら目的のプログラムを作成します。蓄積されたテストコードがないとバグを修正したり既存の実装を変更したりすることで、いままでちゃんと動いていたプログラムが動かなくなったりしないか不安になりますが、テスト駆動開発では蓄積されたテストコードがあるため安心してコードを修正できます♪


テスト駆動開発のポイントをまとめると次のようになるでしょうか。


(1) 最初にテストコードを書く

(2) 失敗するテストケースを考える

(3) 最後までやりとげる


まず、先にテストコードを書くこと(よく『Test First』といわれます)。それから、どれだけ多く失敗するテストケースを思いつくことが出来るかが重要になります。そして、テストを書くのが面倒になって途中でテスト駆動開発を止めてしまわないことです。