t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

VBAで外部ツールを使わずに簡易テスト駆動開発をやってみる。

最近会社の会議でTDDが話題になった。
TDDとは何か?まずWikipediaを引用してみる。

テスト駆動開発 (てすとくどうかいはつ、test-driven development; TDD) とは、プログラム開発手法の一種で、プログラムに必要な各機能について、最初にテストを書き(これをテストファーストと言う)、そのテストが動作する必要最低限な実装をとりあえず行った後、コードを洗練させる、という短い工程を繰り返すスタイルである。

テスト駆動開発 - Wikipedia

話題になったのは、「プログラムできてないのにテストを書けるの?」ということ。
答えは「Yes」

イメージとしては成果物が満たすべき要件を、最初からテストの形式で定義する感じ。

ちょっとやってみよう。

例えば数値を与えるとExcelの列記号に変換するプログラムを考える。

成果物イメージ

以下は成果物をイメージしやすくするためのサンプルコード。
フェイクなので、1~26(A~Z)しか対応していない。

コード

Sub FakeNumToC()
    n = InputBox("数値を入力してください")
    MsgBox "列番号は" & Chr(Asc("A") - 1 + n) & "です。"
End Sub

実行結果

数値入力を求められ、
f:id:t-hom:20190811105258p:plain

入力すると列番号が表示される。
f:id:t-hom:20190811105315p:plain

ただしコードをテスト可能にするには、メインコードからロジックを分離して、ロジック部分を関数にしておく必要がある。
先ほどの成果物イメージで説明すると、以下のようなコードになる。

Sub Main()
    n = InputBox("数値を入力してください")
    MsgBox "列番号は" & FakeNumToC(n) & "です。"
End Sub

Function FakeNumToC(n) As String
    FakeNumToC = Chr(Asc("A") - 1 + n)
End Function

テスト駆動開発の準備

以下のコードがテスト駆動開発の準備。
先ほどのコードと類似しているが、一から作ったという体裁を想定している。
Fakeが付いてなかったり、戻り値が適当だったり、TestNumToCというプロシージャが追加されていたりする。

Sub Main()
    n = InputBox("数値を入力してください")
    MsgBox "列番号は" & NumToC(n) & "です。"
End Sub

Function NumToC(n) As String
    '適当な戻り値
    NumToC = "A"
End Function

Sub TestNumToC()
    'ここにテストを書いていく。
End Sub

関数のゴールをイメージする。

テスト駆動を始める前に、NumToCがどうなったら完成なのか?まずはそのゴールをイメージする。
言葉で表すと、「列番号を与えて、正しい列記号が返ってきたら完成」である。

次に正しい戻り値のケースを具体的に挙げてみる。
「1に対する"A"
2に対する"B"
3に対する"C"
4に対する"D"
5に対する"E"
…」
順番に挙げ始めるとキリがない。
だから普通は、以下のようにパターンが変化するタイミングをサンプリングする。

「1に対する"A"
2に対する"B"

26に対する"Z"
27に対する"AA"
28に対する"AB"

52に対する"AZ"
53に対する"BA"

702に対する"ZZ"
703に対する"AAA"
704に対する"AAB"

かなり泥臭い作業である。
ただこの泥臭さがテストの本質なのでそこは諦めるしかない。

テストを書く

先ほどのコードのうち、TestNumToCプロシージャにテストコードを書いて

Sub Main()
    n = InputBox("数値を入力してください")
    MsgBox "列番号は" & NumToC(n) & "です。"
End Sub

Function NumToC(n) As String
    '適当な戻り値
    NumToC = "A"
End Function

Sub TestNumToC()
    'ここにテストを書いていく。
    Debug.Assert NumToC(1) = "A"
    Debug.Assert NumToC(2) = "B"
    
    Debug.Assert NumToC(26) = "Z"
    Debug.Assert NumToC(27) = "AA"
    Debug.Assert NumToC(28) = "AB"
    
    Debug.Assert NumToC(52) = "AZ"
    Debug.Assert NumToC(53) = "BA"
    
    Debug.Assert NumToC(702) = "ZZ"
    Debug.Assert NumToC(703) = "AAA"
    Debug.Assert NumToC(704) = "AAB"
End Sub

Debug.Assert命令はFalseを与えると中断モードになる命令である。
ここではNumToC関数に引数を与えて、その戻り値と予想値(イコールの右辺)を比較している。
これで戻り値が異なった場合は、そこで停止してテスト失敗ということ。
書いてる内容は至極シンプル。ただ泥臭い作業である。

実行するとここでテスト失敗。
f:id:t-hom:20190811112659p:plain

NumToCは今のところ常に"A"を返すので、当然失敗する。
ここで失敗するというのはひとつの確認ポイントで、もし中断モードに入らなかったら何かが間違っている。
メインコードができていないのにテストにパスしたらそのテストコードがおかしいということになる。

これでテストファーストは完了。
メインのロジックであるNumToCは全然完成していない。
にもかかわらず、テストコードは書けている。

「プログラムできてないのにテストを書けるの?」
「Yes」

メインコードを書く

ここからは普通にNumToCの中身を書けば良いだけ。
ただし先にテストコードが作ってあるので、検証はすこぶる簡単。

まずは冒頭で作ったFakeNumToCのロジックを流用してみる。

Function NumToC(n) As String
    NumToC = Chr(Asc("A") - 1 + n)
End Function

テストコードを実行すると、以下で止まる。
f:id:t-hom:20190811113507p:plain


ああでもないこうでもないと弄り。。

Function NumToC(n) As String
    If n > 26 Then
        nn = n \ 26
        n = n Mod 26
        
        ret = Chr(Asc("A") - 1 + nn) & ret
    End If
    ret = Chr(Asc("A") - 1 + n) & ret
    NumToC = ret
End Function

やっぱりコケる。
f:id:t-hom:20190811114402p:plain


自前でロジック組むのを諦めてExcelのオブジェクトに頼る。

Function NumToC(n) As String
    NumToC = Split(Cells(1, n).Address, "$")(1)
End Function

これでTestNumToCを実行すると、何も起きなくなった。
実行できてるのか不安なのでテストプロシージャの最後にMsgBox "Test Finished"を入れるようにした。

テスト駆動のメリット

  • テストを先に書くことによってメインコードを書いているときに何度でもテストできるので、タイムリーに間違いを発見でき、手戻りが減らせる。
  • メインコードが書きあがってからもっとスマートにしたいと思ったときに、テストしながらできるのでロジックを壊さずに済む。
  • コードの安全性について説明可能になる。

テストケースの抽出方法

今回はテスト駆動開発の紹介がメインなので、テストケースはかなり大雑把。
専門的には同値分割・境界値分析といった技法があるので詳しく知りたい方は専門書をどうぞ。
※以下の記事で簡単には触れてます。
thom.hateblo.jp

ちなみに今回のケースであれば以下の記事にテストパターンが列挙されているのでオススメ。
www.excel-chunchun.com

以上

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。