最近会社の会議でTDDが話題になった。
TDDとは何か?まずWikipediaを引用してみる。
テスト駆動開発 (てすとくどうかいはつ、test-driven development; TDD) とは、プログラム開発手法の一種で、プログラムに必要な各機能について、最初にテストを書き(これをテストファーストと言う)、そのテストが動作する必要最低限な実装をとりあえず行った後、コードを洗練させる、という短い工程を繰り返すスタイルである。
話題になったのは、「プログラムできてないのにテストを書けるの?」ということ。
答えは「Yes」
イメージとしては成果物が満たすべき要件を、最初からテストの形式で定義する感じ。
ちょっとやってみよう。
例えば数値を与えるとExcelの列記号に変換するプログラムを考える。
成果物イメージ
以下は成果物をイメージしやすくするためのサンプルコード。
フェイクなので、1~26(A~Z)しか対応していない。
コード
Sub FakeNumToC() n = InputBox("数値を入力してください") MsgBox "列番号は" & Chr(Asc("A") - 1 + n) & "です。" End Sub
実行結果
数値入力を求められ、
入力すると列番号が表示される。
ただしコードをテスト可能にするには、メインコードからロジックを分離して、ロジック部分を関数にしておく必要がある。
先ほどの成果物イメージで説明すると、以下のようなコードになる。
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関数に引数を与えて、その戻り値と予想値(イコールの右辺)を比較している。
これで戻り値が異なった場合は、そこで停止してテスト失敗ということ。
書いてる内容は至極シンプル。ただ泥臭い作業である。
実行するとここでテスト失敗。
NumToCは今のところ常に"A"を返すので、当然失敗する。
ここで失敗するというのはひとつの確認ポイントで、もし中断モードに入らなかったら何かが間違っている。
メインコードができていないのにテストにパスしたらそのテストコードがおかしいということになる。
これでテストファーストは完了。
メインのロジックであるNumToCは全然完成していない。
にもかかわらず、テストコードは書けている。
「プログラムできてないのにテストを書けるの?」
「Yes」
メインコードを書く
ここからは普通にNumToCの中身を書けば良いだけ。
ただし先にテストコードが作ってあるので、検証はすこぶる簡単。
まずは冒頭で作ったFakeNumToCのロジックを流用してみる。
Function NumToC(n) As String NumToC = Chr(Asc("A") - 1 + n) End Function
テストコードを実行すると、以下で止まる。
ああでもないこうでもないと弄り。。
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
やっぱりコケる。
自前でロジック組むのを諦めて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
以上