作者: Y さ
日時: 2003/7/17(19:51)
最近、「テスト・ファースト」とか「テスト駆動開発(Test-Driven Developnment)」
とかってお聞きになったことありませんか?
簡単に言うと、
 プログラマはコードを記述する前にテストを書き、
 その後、テストに合格するのに必要なコードだけを書く
といった開発スタイルのことです。

詳細はKent Beck 著「Test-Driven Developnment:By Example」を見てください。
(JavaとPythonのコードが半分くらいを占めています)

自分自身の理解のためにも、テスト駆動開発とはどういうものなのか、簡単な例で
やってみたいと思います。
...それも awk で(^^;)



1.1 準備
ボウリングのスコアデータを以下のように表現するとします。
-----
6,3,0,7,4,4,7,3,6,2,10,0,8,2,10,0,10,0,7,3,10
-----
 ・倒れたピン数を','(カンマ)で区切り、各フレームを2つ(10フレームは最大3つ)の
   組で表す
 ・1〜9フレームでのストライクは 10,0 と表現する
 ・上記ストライク以外の 0 はガター/ミス/ファールを表す
  例えば 0,10 は
   1投目ガター(orファール)で10本のピン全部が残った後、
   2投目でこれを全部倒した事(スペア)を表す

このように表現されたデータを入力するとtotalスコアを計算するプログラムを作る
ことを考えます。


「まだ何をテストするのかさえ分かっていない状態なのに、いったいどうやってテス
トを書けば良いのか? 」についてですが、「プログラムが何をするべきか」、といっ
た姿は思い描けるのではないでしょうか?

例えば、空のデータ=計算するものが無いなら計算不能(-1)が返るべき、といった事
があります。まずはこれについてテストしましょう。

なお、XP(eXtreme Programming)ではxUnit等のテストの自動環境テスティング・フレ
ームワークが利用されますが、今回は、単純なテスト実行結果が確認できる骨組みを
作ってみます。
-----
# テストルーチン
function test(data,stat,val,  ret,err){
  err=0;
  ret=calcScore(data);
  if(stat=="==" && !(ret==val)) err=1;

  printf("%s: ", ((err)?("err"):("OK ")));
  print "calcScore(\"" data "\")=" ret,stat,val;
}
BEGIN{
 test("","==",-1);    #00:スコア無し(呼び出しテスト)
}
-----
"関数calcScore()を適当なテストケースで呼び出し、期待した結果かどうか"を表示
する、といったものです。

とりあえず関数calcScore()は以下のようにします。
-----
# ボウリングのスコア計算
function calcScore(data,  sc){
  sc=-1; #失敗(仮)
  return sc;
}
-----
...雛型なので-1を返すだけで具体的な事は何もしません(^^;)

とにかく実行してみます。
-----
C:\>mawk32 -f b.awk
OK : calcScore("")=-1 == -1
-----
1つのテストケースが実行されテストは全て問題なく終了しました。

なお、問題なく実行できることが確認できたらわざとテストを失敗させてみます。
特に今回のようにテストの仕組みも作った場合は、結果表示がバグっていない事を確
認するためにも必要です。
-----
function calcScore(data,  sc){
  sc=0; #成功?(仮)
  return sc;
}
-----
ここでは、-1を0に変えてわざとテストを失敗させます。

-----
C:\>mawk32 -f b.awk
err: calcScore("")=0 == -1
-----

期待値が-1なのに0だったというメッセージが出ています。
確認できたら、元に戻しておきましょう。またテストOKになりました。



1.2 関数calcScore()の実装
それではテスト駆動開発(TDD)でスコアデータを読み込んで計算する関数を実装して
みましょう。
ちなみに、スコア計算ルールは、
 ・ストライクの場合は,10+次の2投の合計を加算する
 ・スペアの場合は,10+次の1投の合計を加算する
 ・それ以外の場合は,1フレーム中で倒したピン数を単純に加える
 ・10フレーム目は変則で,2投あるいは3投した合計をそのまま加算する
   (10フレームが3投分ある理由は
    1投目でストライクだった場合の次の2投分、
    スペアだった場合の次の1投分 を加算できるようにするため)
といった感じです。

TDDでまず最初におこなうのはテストケースの追加です。
2投の合計を返す、といったケースを確認するとしましょう。
-----
 test("5,4","==",9);  #01:2投、スペア無し
-----

プログラムはこう動くはずだ、という考え方が頭に浮かんだとしても、実装について
考えるのは後回しにしましょう。まずはテストを書き、テストを実行するのです(も
しかしたら、一発ですぐ通ってしまうかもしれません)。
その後に実装の方に目を向けましょう。

-----
C:\>mawk32 -f b.awk
OK : calcScore("")=-1 == -1
err: calcScore("5,4")=-1 == 9
-----
当然、追加したテストケースでは期待した9が返っていない、といったエラーになり
ました。また、最初のテストケースはエラーにはなりませんでした。
あえてテストを実行し予想した状態(エラー)になることを確かめ、現在の段階では間
違えていないという確信を得ます。

さて、このテストが失敗している状態をTDDではRedと呼びます。
(例えば、Java用のxUnitであるJUnitのGUIでは赤いバーが表示されます。)

それでは、処理を定義することにします。
-----
function calcScore(data,  sc,tbl,cnt){
  cnt=split(data, tbl, ",");
  if(cnt==0) return -1;

  sc=0+tbl[1]+tbl[2]; # Fake It
  return sc;
}
-----

-----
C:\>mawk32 -f b.awk
OK : calcScore("")=-1 == -1
OK : calcScore("5,4")=9 == 9
-----
テストは成功しました。

冗談でこんな実装をした訳ではありません。これはTDDでFake Itと呼ぶ定石です。
もっとも一瞬で正しい実装をする自身があればFake Itを省略して実装してしまって
もかまいません。

なお、テストが成功した状態をTDDではGreenと呼びます。
(JUnitなら緑のバーが表示されます。)


TDDでは実装コードを抽象化するために、重複するテストケースではなく、別の視点
からのテストケースを追加する、Triangulate(三角測量)という定石があります。
そこで今度は、スペア計算を正しく行う事ができるかテストを追加してみましょう。
-----
 #02:4投、スペア後、スペア無し
 test("9,1,5,4","==",24);
-----

-----
C:\>mawk32 -f b.awk
OK : calcScore("")=-1 == -1
OK : calcScore("5,4")=9 == 9
err: calcScore("9,1,5,4")=10 == 24
-----
<<Red>>

...当然失敗します。では、スペア計算処理を定義してみましょう。
-----
function calcScore(data,  sc,tbl,cnt){
  cnt=split(data, tbl, ",");
  if(cnt==0) return -1;

  # Fake It
  sc=0;
  if(tbl[1]+tbl[2]!=10){ # ストライク/スペアでは無い
    sc=tbl[1]+tbl[2];
  }else if(tbl[1]+tbl[2]==10 && tbl[2]!=0){ # スペア
    sc=tbl[1]+tbl[2]+tbl[3];
  }

  sc+=(tbl[3]+tbl[4]);

  return sc;
}
-----

-----
C:\>mawk32 -f b.awk
OK : calcScore("")=-1 == -1
OK : calcScore("5,4")=9 == 9
OK : calcScore("9,1,5,4")=24 == 24
-----
<<Green>>


テストは成功しました。
続けてストライクについてのテストをしてみましょう。
-----
 #03:4投、ストライク後、スペア無し
 test("10,0,5,4","==",28);
-----

-----
C:\>mawk32 -f b.awk
OK : calcScore("")=-1 == -1
OK : calcScore("5,4")=9 == 9
OK : calcScore("9,1,5,4")=24 == 24
err: calcScore("10,0,5,4")=19 == 28
-----
<<Red>>

ストライク計算処理を定義し、テストを実行します。
-----
function calcScore(data,  sc,tbl,cnt){
  cnt=split(data, tbl, ",");
  if(cnt==0) return -1;

  # Fake It
  sc=0;
  if(tbl[1]+tbl[2]!=10){ # ストライク/スペアでは無い
    sc=tbl[1]+tbl[2];
  }else if(tbl[1]+tbl[2]==10 && tbl[2]!=0){ # スペア
    sc=tbl[1]+tbl[2]+tbl[3];
  }else if(tbl[1]+tbl[2]==10 && tbl[2]==0){ # ストライク
    sc=tbl[1]+tbl[3]+tbl[4];
  }

  sc+=(tbl[3]+tbl[4]);

  return sc;
}
-----

-----
C:\>mawk32 -f b.awk
OK : calcScore("")=-1 == -1
OK : calcScore("5,4")=9 == 9
OK : calcScore("9,1,5,4")=24 == 24
OK : calcScore("10,0,5,4")=28 == 28
-----
<<Green>>


テストは成功しました。
うまくいくようですので、次はストライク2連続のテストをやってみます。
-----
 #04:6投、ダブル後、スペア無し
 test("10,0,10,0,5,4","==",53);
-----

-----
C:\>mawk32 -f b.awk
OK : calcScore("")=-1 == -1
OK : calcScore("5,4")=9 == 9
OK : calcScore("9,1,5,4")=24 == 24
OK : calcScore("10,0,5,4")=28 == 28
err: calcScore("10,0,10,0,5,4")=48 == 53
-----
<<Red>>

処理を定義します。
-----
function calcScore(data,  sc,tbl,cnt){
  cnt=split(data, tbl, ",");
  if(cnt==0) return -1;

  # Fake It
  sc=0;
  if(tbl[1]+tbl[2]!=10){ # ストライク/スペアでは無い
    sc=tbl[1]+tbl[2];
  }else if(tbl[1]+tbl[2]==10 && tbl[2]!=0){ # スペア
    sc=tbl[1]+tbl[2]+tbl[3];
  }else if(tbl[1]+tbl[2]==10 && tbl[2]==0){ # ストライク
    if(tbl[3]+tbl[4]==10 && tbl[4]==0){ # 連続ストライク
      sc=tbl[1]+tbl[3]+tbl[5];
    }else{
      sc=tbl[1]+tbl[3]+tbl[4];
    }
  }

  if(tbl[3]+tbl[4]!=10){ # ストライク/スペアでは無い
    sc+=(tbl[3]+tbl[4]);
  }else if(tbl[3]+tbl[4]==10 && tbl[4]==0){ # ストライク
    sc+=(tbl[3]+tbl[5]+tbl[6]);
  }

  sc+=(tbl[5]+tbl[6]);

  return sc;
}
-----

-----
C:\>mawk32 -f b.awk
OK : calcScore("")=-1 == -1
OK : calcScore("5,4")=9 == 9
OK : calcScore("9,1,5,4")=24 == 24
OK : calcScore("10,0,5,4")=28 == 28
OK : calcScore("10,0,10,0,5,4")=53 == 53
-----
<<Green>>


うまくいったようです。

コードを拡張する際、古いテストがきちんと通るようにすることは、新しいテストを
きちんと通るようにすることと同じくらい重要です。
(続く)