Test Driven DeveopmentとDesign By ContactとAll Pair Testing

Test Driven Deveopment (TDD) とDesign By Contact (DbC)とAll Pair Testingを組みわせたら最強の開発プロセスを実現するテスティングフレームワークが作れるのではないかと思った。だらだらとまとまりなく書きます。

TDDは仕様を決定したら、先にテストを書き、テストが通るようにコードを書く。テストが不合格ならテストが間違っているかコードが間違っている。だから、どちらかを直せばよい。テストが通れば両方が正しい。両方が間違っている場合もありうるが、正解は1個だが間違いは多様なので両方を同じように間違えることは無視していいほど小さい。仮に両方を同じように間違えるときは仕様を誤解していたときだけで、それは人間のバグで手が滑ってコードを書き間違えたようなケースとは別の問題となる。少なくともサブルーチンレベルでは単純な間違いをなくせる非常に有効な手段だ。

DbCもTDDと似たようなコンセプトだと思っている。どちらもサブルーチンが満たすべき仕様を決めておいて、機械によって仕様を満たしているかどうかを判断させようとする。assertでコード内に仕様を規定するものを埋め込むのか、unit testで入出力の仕様を規定するのかといった手続きは違うのかもしれない。assertを使うのはunit testがなかったというだけの理由なのか?unit testでもContractは書ける。

両者が異なるかなと思うのは、TDDはどういうテストを書くべきかということについては答えてくれないところだ。TDDはコーディング手法あるいはテスト手法という側面が強いという印象がある。しかし、DbCはDesignと付くぐらいだから設計手法だ。何かしらを決定することがソフトウェアの設計だと思っているが、設計手法というのは何を決めるべきかという問だろう。

設計とテストは分けるべきものでない。V字モデルだと設計とテストが対になっているとする。だったら設計ができていたら自動的にテストも終わるではないか。設計書を書く意義は分からんでもないけれど、テスト表は理屈からしたら要らないじゃん。真面目に設計書を作っていたら、テスト表は機械的に作ればいいだけという話であれば、機械的に出来るものを人間様にさせるのは馬鹿げている。私はそんな無駄な仕事はしたくない。テスト表をもって設計書とするか、設計書からテスト表を生成するようにしたい。DbCとTDDを融合させるとそれらしい手法になる気がする。

とはいってもDbCは私もちゃんと理解していない。DbC形式言語とか持ち出してくるから親しみにくいのだと思うのだけれど、形式言語で書かれた仕様書なんて人間も機械も読めないのだから、多分そこは重要でない。きっとポイントだと思う点は次だ。サブルーチンの仕様というのは

  • 事前条件
  • 事後条件
  • 不変条件

の3つに分けれれてそれらを決定すれば良い。事前条件はサブルーチンを呼ぶ出す側が満たすべき条件で、これが満足されたときにサブルーチンは呼ぶ出されると事後条件を満たすことを保証する。事前条件が満足されていないとき、あるいは事後条件を満足できないときはエラーを吐いて落とす。

書いていてしっくりこないなあと思うのだけれど、おそらくオブジェクト指向プログラミングでメソッドを呼ぶ出す場合には、不正な事前条件が実現できてしまうことも呼び出され側のクラスの問題だからなのだと思う。不正な状態を作れることが誤りだろう。nullを入力するとかは勝手に落ちるから考えなくていい。また、事後条件が満足できないときの動作は、それも含めても事後条件にならないか。

そうすると事前条件と事後条件の対応だけの問題になってユニットテストが書けそうだ。事前条件と事後条件という単語はしっくりこない。原因と結果と訳した方が日本語としては適切そうなので、以降は原因と結果、その対応を因果関係と書く。原因と結果は状態機械を想定したら状態と入出力に分けられるだろう。つまり、

  • 原因
    • 事前状態
    • 入力
  • 結果
    • 事後状態
    • 出力

となるでしょう。因果関係が全て記述できたらサブルーチンの設計はできていることになるのだろう。

因果関係も膨大なので、何かでグルーピングしないと整理しきれない。サブルーチンの設計をしたいのが通常だと思うので、普通は入力で因果関係をグルーピングするだろう。つまり、サブルーチン(メソッド・関数)ごとに因果関係が記述される。なので入力とは引数のバリエーションになる。場合によっては標準入力や読み込むデータの内容も入るだろう。*1

事前状態は、フィールド値・DBの状態・設定値等だろう。unit testのsetup, teardownは事前状態を作るときの初期状態を共通化するために使う。

出力は返り値、Exception、errno、Exit Code、標準出力等だろう。事後状態の内訳は事前状態と対応すると思われる。assertXXX関数でこれらを評価する。

不変条件はサブルーチンの前後で変わらない条件で、JUnitだと@BeforeClassですることがそれでしょう。@BeforeClassだけでは足りなくて、環境構築も不変条件に含まれそうだ。ウォーターフォール的発想で行くと、不変条件は前の工程で決まっているべきことに思える。前工程ですでに事前条件を作れるテストコードが作られているということだ。(そんなの出来るのか?ここではウォータフォール開発自体の是非は説かない)

以上の因果関係を全部決定できたら設計は完了したと言えそうだ。しかし、まだ設計とテストの谷を埋めるには3つほど問題が残っている。

  1. コードを書くときに必要なテストの数と正しさを証明するときのテストの数は違う
  2. 正しさを証明ための全部のテストコードなんて書けない
  3. 条件が増えるときはどうするのか

コードを書くときに必要なテストの数と正しさを証明するときのテストの数は違う。V字モデルが正しいなら、設計書とテスト表は1対1になっていて互いの分量は同じはずだ。しかし、実際は異なる。テスト表の方がボリュームが大きいのが常だ。組み合わせとそれに伴う重複の問題だ。同じことをやっているのだけれど、組み合わせ的にはテストの必要がある。結果、重複したテストが必要になる。TDDでやるとしたら、最初から十分なテストを用意するか、足りないテストは後から増やすか2通りがありうるだろう。いずれにしても十分だとか足りないだとかの判断が必要になる。

足りる足りないの指標問題は大きすぎる課題だ。完全とかありえないから、どこかで切り上げるしかない。これは統計の問題だ。統計的に大丈夫そうな指標と値を見つけるしかない。統計的な裏付けもなしに議論したら、際限なく求められるテストが増える。感覚的なこんなもん無意味だという主張と根拠のないやれという主張は絶対に交わらない。一般に手を汚さない人が根拠なくやれって言うから、やらされる方は無意味なことをしていると思ってやるわけで精神が蝕まれる。指標として考えられるのは、

  • 人日(かかった手間)
  • テストケースの数
  • カバレッジ(構造網羅率、C0やC1)
  • 原因の組み合わせの網羅率(仕様網羅率)

であろう。人日はありえない。テストを自動化したいのに労力を指標にはできない。行数当たりのテストケースの数は分からないでもないが、この値の基準値を出そうと思ったら、十分そうなテストを行った実績値の平均を持ってくるしかない。結局十分そうかどうかは別の指標が必要になる。カバレッジは悪くない指標だ。しかし、これはコードを書いた後でないと評価できない。テストファーストで最初から十分なテストを準備したいときには使えない。足りなかったところをコードを書いた後にテストを足す場合には有効だろう。したがって、十分なテストをコードを書く前に用意するなら原因の組み合わせの網羅率から十分性を判断するしかないように思う。

原因の組み合わせを網羅したテストを先に書くことなんてできるのか。本当に全ての組み合わせを網羅したら、組み合わせ爆発を起こすのでありえない。意味のありそうなものを抽出して行うのが普通だ。意味のありそうなものを抽出を人間がするのはセンスがない。いかにも機械でできそうなことだ。ちょうどそういう手法が存在した。All Pair Testingという。全ての組み合わせは無理なので、2つのパラメーターの値の組み合わせは網羅しようというものである。経験的にバグが発生する条件は、1つのパラメータがある値をときか、2つのパラメーターがある値の組をとったときが、ほとんどである。3つ以上のパラメータが関わるバグというのは非常にすくない。したがって、2つのパラメーターの値の組み合わせを網羅すれば十分なテストと呼べる。科学的根拠があって合理的だ。原因のパラメータと取りうる値を入力すると、テストすべき原因の組み合わせを生成するツールが存在する。Microsoft謹製の「PICT」等があります。*2

原因に対して結果を埋めていく作業は人間しかできなさそうだ。それを機械にやらせたらテストされる側のコードを作ることになってしまう。これを埋める作業が設計になるのでしょう。

手続きとしては次のようになりそうだ。

  1. 原因のパラメータと取りうる値を「機械が読める形式」で列挙する。
  2. テストするべき原因を機械が自動生成する。
  3. 原因に対応する結果を人間が決める。成果物は「機械が実行できる」テストコードの形式になっている。

中間生成物としてExcelとかがあってもいいが、中間生成物を作ったら整合性を取る作業が増えるだけなのであまりやりたくない。

また、テストコードはある程度自動生成が出来そうだ。特に事前状態をつくるところだ。部品として事前状態をつくる関数を用意しておいて、その関数を機械が呼び出す形式にしたい。それっぽいコードを自動生成する手もありうるけれど、それをするぐらいならテストフレームワークの方を直した方がいい気がする。

問題は、あとからパラメータや値のバリエーションを増やしたときにテストが全部作り直しにならないようにすることだ。手戻りは嫌がられるが、私は悪いことだとは全く思っていない。手戻りをしたときに全部やりなおさなければならないのだとしたら、開発プロセス自体が間違っているんだ。手戻りで直したときの差分だけ修正しても整合性が壊れないようにしたい。多分テストケースの生成ツールで上手いことできる気がする。

だらだら書いたけれど、こういうことできないのかな?

*1:データが読めないときを入力とするか事前状態とするかは場合によりそう

*2:ちょっと古い。まだ試していないので今度試す。実行ファイルは http://download.microsoft.com/download/f/5/5/f55484df-8494-48fa-8dbd-8c6f76cc014b/pict33.msiから取れる。