作者: Yさ
日時: 2005/4/1(03:12)
拙者、ユニットテスト侍じゃ。
(http://akiyah.bglb.jp/blog/474)



 「テスト駆動開発(Test-Driven Developnment)」ってご存知ですか?
 簡単に言うと、
  プログラマはコードを記述する前にテストを書き、
  その後、テストに合格するのに必要なコードだけを書く
といった開発スタイルのことです。
 詳細はKent Beck 著「Test-Driven Developnment:By Example」を見てください。
 (JavaとPythonのコードが半分くらいを占めています)

 テスト駆動開発を、簡単な例でご紹介してみたいと思います。...awkで。


1.1 準備
 10進数値を与えると漢数字表現に変換する関数 cnvJNum() を作るとします。
 XP(eXtreme Programming)ではxUnit等のテストの自動環境テスティング・フレーム
ワークが利用されますが、ここでは単純なテスト実行結果が確認できる骨組みを
作ってみます。
 "関数を適当なテストケースで呼び出し、期待した結果かどうかを判定する"
といったものです。

----- [jntest.awk] -----
# 漢数字変換
function cnvJNum(num){
  return "";
}


# ----- 以下はテスト用 -----
function CLR_RESULT() {TestCnt_=ErrCnt_=0;
  print "ユニットテースト、ユニットテスト♪"
  print "全てのテストを自動実行しまくるぜ、って言うじゃなぁい?\n"
}
function CNT_RESULT(sw) {++TestCnt_; if(sw) ++ErrCnt_;}
function CHK_RESULT() {
  if(ErrCnt_==0)
    printf("\n... 拙者、%d個成功して安心しきってますから。切腹!!\n", TestCnt_);
  else{
    printf("\n... でもあんたのテスト、失敗しちゃってますから! 残念!!\n");
    printf("%d個のテストで、失敗%d個、斬り!\n", TestCnt_, ErrCnt_);
  }
}
function eval(fname,val,stat,test,  err){
  err=0;
  if(stat=="==" && !(val==test)) err=1;
  if(stat=="!=" && !(val!=test)) err=1;

  if(err) print "err: " fname "=[" val "]",stat,test;

  CNT_RESULT(err);
}


BEGIN{
  CLR_RESULT();

  eval("#00: 0",cnvJNum("0"),"==","〇");

  CHK_RESULT();
}
-----

 実行すると、
=====
bash-2.05$ gawk -f jntest.awk
ユニットテースト、ユニットテスト♪
全てのテストを自動実行しまくるぜ、って言うじゃなぁい?

err: #00: 0=[] == 〇

... でもあんたのテスト、失敗しちゃってますから! 残念!!
1個のテストで、失敗1個、斬り!
=====

 期待値が"〇"なのに""だったというメッセージが出ています。(詳細は後述します)

 とりあえず関数cnvJNum()を以下のようにしてみます。
-----
function cnvJNum(num){
  return "〇";
}
-----

=====
bash-2.05$ gawk -f jntest.awk
ユニットテースト、ユニットテスト♪
全てのテストを自動実行しまくるぜ、って言うじゃなぁい?


... 拙者、1個成功して安心しきってますから。切腹!!
=====

 テストは問題なく終了します。

 ではテスト駆動開発(TDD)で10進数値を与えると漢数字表現に変換する関数を実装して
みましょう。


1.2 関数cnvJNum()の実装
 TDDではまず最初にテストケースを追加します。
-----
  eval("#01: 1",cnvJNum("1"),"==","一");
  eval("   : 2",cnvJNum("2"),"==","二");
  eval("   : 3",cnvJNum("3"),"==","三");
  eval("   : 4",cnvJNum("4"),"==","四");
  eval("   : 5",cnvJNum("5"),"==","五");
  eval("   : 6",cnvJNum("6"),"==","六");
  eval("   : 7",cnvJNum("7"),"==","七");
  eval("   : 8",cnvJNum("8"),"==","八");
  eval("   : 9",cnvJNum("9"),"==","九");
-----

 省略しますが、テストを実行し、失敗することを確認します。

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

 最もシンプルな、テストをパスしそうな実装をしてみます。
-----
function cnvJNum(num){
  if(num=="1") return "一";
  if(num=="2") return "二";
  if(num=="3") return "三";
  if(num=="4") return "四";
  if(num=="5") return "五";
  if(num=="6") return "六";
  if(num=="7") return "七";
  if(num=="8") return "八";
  if(num=="9") return "九";
  return "〇";
}
-----

 (省略しますが)テストは成功します。

 TDDではテストが成功している状態をGreenと呼びます。
 JUnitなら緑のバーが表示されます。

 TDDではコードに重複があるならリファクタリングの余地が無いか見直します。
 if文とreturnが繰り返され重複しているのに気がつきます。それを解消します。

-----  [リファクタリング途中、name[]の導入]
char *cnvJNum(char *num)
{
function cnvJNum(num,  name){
  split("一,二,三,四,五,六,七,八,九",name,","); name[0]="〇";
  if(num+0==1) return name[1];
  if(num+0==2) return name[2];
  if(num+0==3) return name[3];
  if(num+0==4) return name[4];
  if(num+0==5) return name[5];
  if(num+0==6) return name[6];
  if(num+0==7) return name[7];
  if(num+0==8) return name[8];
  if(num+0==9) return name[9];
  return name[0];
}
-----
 ↓
-----  [if,returnの重複除去、他]
function cnvJNum(num,  name){
  split("一,二,三,四,五,六,七,八,九",name,","); name[0]="〇";
  if(1<=num+0 && num+0<=9) return name[num+0];
  return name[0];
}
-----

 省略しますがリファクタリング途中で、テストが失敗しないか常に確認します。
 全てのテストに成功する事で、まずい事が起きていないのが確認できます。

 なお "機能の拡張"と"リファクタリング"は異なる概念なので、きちんと区別して
考える必要があります。TDDでのリファクタリングは重複の除去についてのみです。
 (蛇足:作り直しは"リストラクチャリング"です。)


 次に、'十'(10)を扱えるように拡張してみましょう。
 まずテストケースを追加します。
-----
  eval("#02: 10",cnvJNum("10"),"==","十");
-----

 (省略しますが)当然テストが失敗します。

 とりあえず1桁以外だったら無条件で'十'(10)を返すようにロジックを追加してみます。

----- [Fake]
function cnvJNum(num,  name){
  if(length(num)==1){
    split("一,二,三,四,五,六,七,八,九",name,","); name[0]="〇";
    if(1<=num+0 && num+0<=9) return name[num+0];
    return name[0];
  }
  return "十";
}
-----

 (省略しますが)テストは成功します。

 これはTDDでFake Itと呼ぶ定石です。
 Fakeの後は、別の視点からのテストケースを追加し、テストを失敗させ、次に成功するようにロジックを追加します。
-----
 :
  eval("   : 11",cnvJNum("11"),"==","十一");
  eval("   : 19",cnvJNum("19"),"==","十九");
-----

 2桁を分解して再帰的に変換する事にします。

----- [2桁ロジック追加中]
function cnvJNum(num,  name){
  if(length(num)==1){
    split("一,二,三,四,五,六,七,八,九",name,","); name[0]="〇";
    if(1<=num+0 && num+0<=9) return name[num+0];
    return name[0];
  }
  return "十" cnvJNum(substr(num,2));
}
-----

=====
 :
err: #02: 10=[十〇] == 十

... でもあんたのテスト、失敗しちゃってますから! 残念!!
13個のテストで、失敗1個、斬り!
=====

 11,19のテストは成功します。が、今度は10で失敗するようになってしまいました。
 どうやらただの0と区別して扱わないとダメなようです。

----- [関数を二段構成とする、他]
function cnvJNum_(num,  name){
  if(length(num)==1){
    split("一,二,三,四,五,六,七,八,九",name,","); name[0]="";
    if(1<=num+0 && num+0<=9) return name[num+0];
    return name[0];
  }
  return "十" cnvJNum_(substr(num,2));
}
function cnvJNum(num){
  return (num+0==0)?"〇":cnvJNum_(num);
}
-----

 (省略しますが)テストは成功します。

 続けて、20以上を扱えるように拡張してみましょう。
 テストを追加します。
-----
 :
  eval("   : 20",cnvJNum("20"),"==","二十");
  eval("   : 21",cnvJNum("21"),"==","二十一");
  eval("   : 22",cnvJNum("22"),"==","二十二");
  eval("   : 99",cnvJNum("99"),"==","九十九");
-----

 テストが成功するように、ロジックを追加します。
-----
 :
  wk=substr(num,1,1);
  buf=(wk=="1")?"":cnvJNum_(wk);
  return buf "十" cnvJNum_(substr(num,2));
}
-----

 (省略しますが)テストは成功します。

 [後編に続く]