TDDを理解するためのまとめ

わんくま同盟名古屋勉強会#9に置いて、biacさんのTDDに関する話が出たので、少し自分がTDDについて思うことを纏めてみました。
TDDが説明されるのを聞く度、見る度、多分説明している本人は分かっているのだろうけれど、それが他の人に本当に伝わっているのかが怪しいと思ったためです。
というのも、自分が(多分)理解するまでに、酷い回り道をしたもので。


また、biacさんのTDDに関するWebサイトはこちら。
TDD.NET - http://www.tdd-net.jp/


以下、長文注意。

背景

まず、自分がTDD(より正確に記述するなら、「テストファースト*1」が正しく、TDDではない)をまともに実践しようと思って始めたのが、大学の4年時の最初なので、今から18ヶ月程度前です。
とある研究室のプロジェクトで使いたいという話になり、そこで実践を行いました。当時の環境はJDK + JUnitです。
しかし、そのテストファーストは、自分にとっては酷い結果であると言わざるを得ませんでした。
当時の環境は次の通りです。

  1. 私自身がテストファーストやTDDについて無知であった。というか、誰もこれらについてまともに理解していない
  2. テストファーストで仕様書を記述してもよい、と言われていたにも関わらず、別ファイル(Word, Excelなど)の資料を要求される
  3. 何をテストしなければならないかは仕様書が既に出来ている
  4. 徹底的な(いわば無駄な)テストを要求される
  5. 同じようなテストコードをコピペして使うことを推奨される
  6. 1メソッドには1Assertのみ。それ以上のAssertを記述することは許されない
  7. メソッド、コンストラクタ、変数にはそれぞれの先頭文字(m/c/v)+[メソッド連番]のような番号が振られており、テストメソッド名の付け方はコレに基づかなければならない
  8. テスト対象クラスのgetter/setterすら信用してはならない。privateなフィールドはリフレクションによって取得し、テストを行う

これは酷い。今見ても失敗しない理由が何処にもないですね。


なお、後で聞くと、テストファーストという言葉を聞いて試して見たかった、との返答を企画者より頂いていますので、そちらもTDDという分野においてほとんど無知であったということを付け加えておきます。そりゃ失敗しても当たり前ですよね。まあ、お金が動くというわけでも無かったので、苦すぎる経験をした、ということで1つ。……いや、一度テストがマジでキライになりそうでしたが。


このプロジェクトのテストコードの実装には、本来のコードを実装するときにかかった時間よりも6倍多くの時間がかかりました。
テストと言うには無駄で、大きすぎると言うには十分な結果だと思います。
このようなTDDもどきを体験した結果、逆に、「何故テストファーストは良いと言われ、流行っているのだろう」と考え、自分でTDDに関して、もう一度勉強を始めました。


最初に、いくつかのWebサイトや、Kent Beck氏の書籍テスト駆動開発入門を読んで見たのですが、やはり私にはTDDの感覚が理解出来ませんでした。
と、いうのもそこに記述されている例だと、後でテストを書く方が有利に思われたからなのです。
ある意味では残念な事なのですが、大抵のTDDの説明に記述されている、「RedからGreenに変わることが気持ちいい」という、その感覚が分からないのです。なお、今は別にどっちでも良いというか、そんなに気にしてません。


TDDの紹介では、大概の場合に置いて小さなメソッドのテストが行われます。そこで説明されている事項は、説明のためとは言え、明らかに易しすぎるのです。その程度の事なら、テストを書く前に実装をしたい、と思ってしまうのです。
確かに、先にテストを書くことで以降のテストのためのコードは残ります。後からテストを書くのを「やっぱりやめた」と言って、テストコードを書かないことを防ぎます。そして、そのテストを残す事がテストファーストの利点と言われます。
しかし、そう言われても、何か釈然としなかったのです。


その理由の1つは、例ではテストを単一メソッドに対して行っており、そのテストからスケルトンコード(REDを出力する実装)を生成していたことです。
メソッドはクラスの一部であり、そのクラスの振る舞いを定義します。
しかし、メソッドは詳細です。ただ1つのメソッドでは、クラス全体の振る舞いを知ることは出来ません。
故に、クラスという大ではなく、メソッドという小に焦点を当てているにも関わらず、そこからクラス全体をテストするためのコードを生み出そうとしているように見えることが、釈然としませんでした。
(注:記事の投稿時に確かめてみると、おそらくテスト駆動開発入門ではこれとは違う方法で生成を行っています。この本のPart1は詳細に進めすぎていて、逆に全体が見えてこない印象を受けましたが…)


そのような小をテストすることが単体テストである、と言われたらそれまでなのですが……
私は、そのような小をいきなり作ってしまうということに違和感を感じたのです。

シナリオテストとテストファースト「シナリオ」

http://www.morijp.com/masarl/homepage3.nifty.com/masarl/index.html
http://www.morijp.com/masarl/homepage3.nifty.com/masarl/article/junit/scenario-based-testcase.html

石井勝氏のページに出会ったのが、テストファーストで使用するJUnitに関して調べていたときでした。
リンク下の先で述べられている、「メソッドベースとシナリオベース」。
これが私の違和感、考えを纏め、正しいと思われる方向に導く手助けをしてくれました。


多くのテストファーストの説明に置いて、私はそれらの文脈から「メソッドベース」のみを先に記述する様にしか受け取れませんでした。
一方で、シナリオベースのテストこそが、まさにテストファーストが行うべき手法ではないかと思います。


クラスに関わらず、プログラムの1つの問題として「実行される順序が重要」というものがあります。
例えば、ファイル処理であれば、「ファイルを開く→使用する→閉じる」という、決められた順で処理を行う必要があります。これを1つでもずらすと、正確な処理は出来ません。
事実、例えばデザインパターンのTempleteMethodやFacadeなどは、まさにこの順序を保証するためのパターンです。


作成されたクラスにも同様のことが言えます。
「どのように使われるか」という事は、クラスを作る時点に置いては分かっているはずなのです。
上記のファイルの例をプログラムで書けば、以下の様になるでしょう。

// 文法はJava。例外は考慮しない。
MyFile f = new MyFile("hoge.txt"); // open
String data = f.getAllContext();   // get data for Using
f.close();                         // close

これが、シナリオであり、クラスがどのように使われるかを定義したものです。
この様なシンプルな記述があるとき、そのテストを見たクラスの利用者は、そのクラスの使い方を知ることができます。そう、つまりこれはクラスの使用方法を記述したマニュアルになるのです。
このシナリオから、全てのメソッドのスケルトンを自動生成することは、自分にとってはすごく自然です。最低限必要な公開すべきインタフェースを決定し、その実装を行うのですから。
逆に、このシナリオに記述できないような公開インタフェースがあれば、ほぼ間違いなくそれは公開の必要がありません。
この時点で、最低限度のテスト(シナリオテスト)が作成されます。スケルトンの自動生成直後はこれが(ほぼ)間違いなくREDです。*2
この後に実装を行い、それをGREENにします。ここまでが、シナリオテストの役目です。
これは、メソッドの単体テストよりも明らかに荒いテストです。しかしおそらく、「クラスの単体テスト」と言い換えることも出来るでしょう。
一般的にクラスは単一責務原則(SRP)を守りますから、クラスを最小単位とみれば、きっとこのレベルのテストでも単体テストと言えるはずです。
補足しますが、もちろん、このテストは1つである必要はありません。後の修正や、あらかじめ考えられるケースに基づき、How to useのシナリオを複数作成するとテストの正確さが上がると考えられます。


そして、用意した全ての(最低限1つの)シナリオテストが終了し、それでも必要であれば、メソッドの単体テストを記述します。
このときは、引数の組み合わせや境界値チェックなどの、一般的な単体テストを記述すべきでしょう。
コレがメソッドベースによる、単体テストです。
このテストを記述するときには、既にメソッドの実装は完了しています。そのため、これはテストファーストと呼べません。
しかし、私はそれがむしろ良いと考えています。むしろ、メソッドベースのテストを実装より先に書く利点が私には余り思いつきません。思いつくのは、RED→GREENが気持ちいいと言った意見と、メソッドの単体テストが確実に残ると言ったことだけです。これを重要視するならば、メソッドベースを記述しても良いのでしょうが……それでも、シナリオベーステストと併せて書くべきだと思います。


時たま、例えばMathクラスのメソッドの様に、1つのメソッドで完結した処理を提供するメソッドがある場合があります。
この場合は、シナリオがただ1つのメソッド呼び出しによって行われているだけであり、シナリオテストとメソッドテストが等価になっています。
そのため、メソッドテスト(だが、正確にはそのように見えるシナリオテスト)をいきなり記述することが、正しいことのように思われることがあります。

シナリオテストによるクラス設計

ある意味では驚くべきことですが、シナリオテストを十分に考慮してプログラミングをすると、設計の質が上がりやすい傾向があります。
これは、そのクラスが外部からどのように使われるかを十分に吟味するためです。
自分がその新しく作られるクラスを使う視点に立つことで、変な使い方をしていないかをチェックすることができます。
例えば、setメソッドが多すぎれば、それをコンストラクタに移したりすることも出来るでしょうし、インタフェースを分離して、別の所でその要素を設定することも出来るかも知れません。
自分は、クラス設計で困ると、テストとは関係無しにシナリオテストによるテストファーストを持ち出してきて、設計を行っています。

まとめ

テストファーストやTDDを説明するとき、こうすれば分かりやすいだろうという流れを以下に纏めてみました。

  1. シナリオテストの作成
  2. テストからスケルトンを作成
  3. テストのREDを確認
  4. ケルトンを実装
  5. テスト全てがGREENであることを確認(実装完了)
  6. 必要であれば、メソッドベースのテストを記述する

これは、ウォータフォールモデルとほぼ対応しています。1〜3が設計、4〜5がコーディング、6がテストとなります。
もちろん、自由に行程を戻って貰って構わないので、そう言った辺りにウォータフォールモデルとの相違点はあります。


自分としては、テストファーストとは「シナリオベースのテストからスケルトンを作成すること」であり、TDDとは「ここで示した手順全体をベースにした開発方法」であると考えています。
というのも、下手なメソッドベースからテストを書き起こすと、痛い目を見たどころの話じゃなかったためです。

結論

  • 自分は、TDD(テストファースト)の説明があまりうまくいっていないと考えている
  • シナリオベースは処理の実行順と流れを、メソッドベースは単体の機能をそれぞれテストする
  • 両方欲しいが、どちらか一方しか書けないならば、シナリオベーステストを先に書いて残す方が後々には役に立つと考えられる
  • テストファーストを用いることで、テストが残ることも利点ではあるが、記述後の設計の質が向上することにも着目すべき。そのためのテストにはシナリオベーステストを用いればよい。


# 何か色々と叩かれる気もするけれど、勇気を出して自分の意見を書き綴ってみたよ!!

*1:以下、明確にTDDとテストファーストを区別します

*2:デフォルト実装が通るという、まれなケースを除けばほぼREDでしょう