概念: テスト ファースト設計
トピック
テストの設計は、ユース ケースの実現、設計モデル、分類子インターフェイスなどの設計成果物も含めたさまざまな成果物から得られる情報を基に作成されます。テストは、コンポーネントの作成後に実行されます。テスト設計の作成は、テストを実行する直前—ソフトウェアの設計成果物が十分に作成された後—に行われるのが一般的です。図 1 に例を示します。ここでは、実装の最終段階に向かうある時点でテスト設計を開始しています。このテスト設計では、コンポーネント設計の結果を使用しています。実装からテスト実行に向かう矢印は、実装が完了するまではテストを実行できないことを示しています。

図 1: ライフサイクルの後半で実行される従来型のテスト設計
しかし、必ずこのようにしなければならないというわけではありません。テスト実行はコンポーネントの実装が完了するまで待たなくてはなりませんが、テストの設計はもっと早い段階でも行えます。設計成果物が完成した直後に行ってもかまいません。また、ここで示すように、コンポーネントの設計と平行して行うことも可能です。

図 2: テスト ファースト設計では、テスト設計をソフトウェア設計と直列式で行う
このようにテスト作業を「上流」に移動させることを、一般に「テスト ファースト設計」といいます。その利点は何でしょうか。
- どんなに注意深く作業したとしても、ソフトウェア設計にミスはつきものです。設計者が何かの関連事実を知らないこともあれば、 設計者の考え方に癖があって特定の代替手段を見つけることが難しくなっている場合もあるでしょう。また、単純に疲れていて何かを見落とすかもしれません。設計成果物をほかの人にチェックしてもらうと、このようなミスをなくす助けになります。チェックする人物が設計者の知らなかった事実を知っているかもしれないし、設計者が見落とした何かを見つけてくれるかもしれません。成果物のチェックは、設計者とは異なる物の見方をする人物が担当するのが最適です。同じ設計を違った視点で見ることにより、設計者が見落とした事実を見つけてくれることでしょう。
過去の経験から、テスト パースペクティブが効果的な方法であることがわかっています。これは、徹底的に具体的な方法です。たとえば、ソフトウェアの設計中には、あるフィールドを「現在の顧客の敬称を表示するもの」と考えた場合、それについて実際に深く考えることなく先に進みがちです。一方、テスト設計では、海軍から脱退した後に法学の学位を取得した顧客が自分のことを「モートン H. スロックボトル海尉 (退役) 殿」と呼んでほしいと主張した場合にそのフィールドに具体的に何を表示するかを決定しなければなりません。彼の敬称は「海尉」でしょうか「殿」でしょうか。 図 1 のように、テスト設計をテスト実行の直前にまで遅らせると、無駄に費用を使うことになるでしょう。ソフトウェア設計での誤りが残されたままプロジェクトが進み、テスト設計に入ってテスト担当者の誰かが「そういえば、私の知人で海軍から脱退した人がいて...」などと言い出して「モートン」のテストを作ったときに初めてこの問題が発見されるのです。こうなって初めて、既に完全にまたは部分的に完成している実装を書き直し、設計成果物を改訂しなければならなくなります。実装を開始するより前に問題を発見できていれば、もっとコストを節約できたことでしょう。
- 誤りのいくつかがテスト設計より前に見つかる場合もあります。ただし、それを見つけるのは実装担当者です。これも、好ましいとはいえません。作業の焦点が、その設計をどのように実装するかという点から設計がどうあるべきかという点に移ってしまい、その間実装作業を停止しなければならなくなるからです。こうなると、たとえ実装担当者と設計者の役割を同一人物が兼任している場合であっても、混乱をきたします。これらの役割を別々の人物が担当していた場合などは、混乱がさらに大きくなります。このような混乱を防ぐのも、テスト ファースト設計のプロジェクトへの寄与の 1 つです。
- テスト設計は、上記とは別の方法でも実装担当者を助けます。設計をはっきりさせるのです。実装担当者は、その設計が何を意味するのかを正確に理解できない場合に、テスト設計から目的とする動作の具体的な例を見ることができます。これにより、実装担当者の解釈間違いによるバグが少なくなります。
- 実装担当者がはっきりとは疑問を抱いていなかった (けれども正しく理解していなかった) 場合であっても、バグの数は少なくなります。たとえば、設計の中に両義に取れる部分があり、設計者は無意識に片方の方法で解釈していたのに、実装担当者は他方の方法で解釈したとします。実装担当者が、設計と、コンポーネントが行うべき処理に関する具体的な指示 (テスト ケースから得られます) との両方を基に作業していれば、実装されたコンポーネントが正に必要とされている動作を行う可能性が高まります。
以降では、テスト ファースト設計の効果がわかるようにいくつかの例を示します。
今まで会議室の割り当てに使っていた「秘書に問い合わせる」という古い手段に取って代わるシステムを作成しているとしましょう。MeetingDatabase クラスのメソッドとして、getMeeting という次のようなメソッドがあります。
Meeting getMeeting(Person, Time);
getMeeting メソッドは、人物 (person) と時刻 (time) を渡されると、その時刻にその人物が出席する予定になっている会議を返します。その人物に予定されている会議がなければ、特殊な Meeting オブジェクトである unscheduled を返します。この例では、次のようないくつかのわかりやすいテスト ケースを作成できます。
- 指定された時刻には、指定された人物に予定されている会議はありません。unscheduled という会議が返されますか。
- その時刻にその人物は会議に出席しています。メソッドは正しい会議を返しますか。
これらのテスト ケースはごくありきたりですが、最終的には必ずテストする必要があるものです。これは、後日実行される実際のテスト コードを記述することによって、今からでも十分作成できます。最初のテストのための Java コードは、次のようになります。
// 指定された時刻に会議が予定されていない場合、
// unscheduled が返されなければなりません。
public void testWhenAvailable() {
Person fred = new Person("fred");
Time now = Time.now();
MeetingDatabase db = new MeetingDatabase();
expect(db.getMeeting(fred, now) == Meeting.unscheduled);
}
一方、もっと興味深いテスト案もあります。たとえば、このメソッドは検索を実行します。メソッドが検索を実行するときには必ず、検索で条件に一致するものが複数見つかった場合にどうすべきかを考えるのがよい慣習です。この例の場合なら、「1 人の人物が 1 度に 2 つの会議に出席することはできるか」と問うことになります。一見不可能に見えますが、このケースについて秘書に問い合わせてみると意外な答えが返ってきます。一部の経営者層の人々にとっては、同じ時刻に 2 つの会議を予定に入れるのは珍しくないということが判明するのです。彼らの役割は、会議に飛び込んで短時間でその場の志気を高めることであり、その後は次の会議へと移動します。このような行動に適応しないシステムは、少なくとも部分的には使用されないことになります。
これは、実装レベルで実行され、分析の問題を捕捉したテスト ファースト設計の一例です。これについては、いくつか注意すべき点があります。
- できれば、優れたユース ケース仕様と分析でこの要件がすでに見つかっていてほしいものです。そうすれば、この問題は「上流で」回避されており、getMeeting は別の形で設計されていたでしょう (1 つの会議を返すのではなく、会議のセットを返すように設計されたでしょう)。とはいえ、分析でも必ずいくつかは問題を見逃すものであり、見逃されたものは開発後に発見されるよりは実装中に発見された方がよいはずです。
- 多くの場合、設計者と実装者には、このような問題を捉えるのに必要な知識もなければ、秘書に詳しく質問する機会も時間もありません。この場合、getMeeting のテストを設計する担当者が、「2 つの会議が返されなければならないようなケースはあるだろうか」と自問し、しばらく考え、そんなケースはないだろうという結論に至るかもしれません。このため、テスト ファースト設計ですべての問題が捕捉できるとは限りませんが、正しい種類の疑問を投げかけるという事実だけでも、問題が見つかるチャンスは増えるはずです。
- 実装中に使用されるテスト手法のいくつかは、分析でも使用できます。テスト ファースト設計も分析でも同様に実行できる手法ですが、これについてはこのページでは取り上げません。
ここでは 3 つの例を示しますが、その 2 つ目は暖房システムのステートチャート (状態遷移図) モデルです。

図 3: HVAC ステートチャート
一連のテストで、ステートチャート内のすべてのアークを経由します。あるテストでは、アイドル (Idle) システムで開始し、高温 (Too Hot) イベントが発生し、クーリング (Cooling) / 稼動 (Running) 状態のときにシステムが停止し、エラーをクリアしてから 2 つ目の高温イベントを発生させ、システムをアイドル状態に戻します。このテストだけではすべてのアークを動かしてみることはできないため、ほかのテストも必要です。この種のテストでは、実装に関するさまざまな問題を探します。たとえば、すべてのアークを経由することにより、実装のし忘れがないかチェックできます。また、失敗するパスの後ろに正常に完了しなければならないパスが続くイベント シーケンスを使用することにより、後の処理に影響する可能性のあるような部分結果のエラー処理コードがクリーンアップ処理に失敗していないかをチェックできます。(ステートチャートのテストについて詳しくは、「 ガイドライン: ステートチャート図とフローチャート図のテスト構想」を参照してください。)
最後の例では、設計モデルの一部を使用します。債権者と請求書の間には関連付けがあり、どの債権者も未払いの請求書を複数件持つことができます。

図 4: 債権者クラスと請求書クラスの関係
このモデルに基づいたテストでは、債権者が請求書を 1 件も持たない場合、請求書を 1 件持つ場合、多数の請求書を持つ場合について、システムを動作させることになるでしょう。テスト担当者は、 1 つの請求書が複数の債権者に関連付けられる場合や、債権者を持たない請求書が発生する場合があるかどうかについても考えます (おそらく、現在紙ベースのシステムで処理している人々は、コンピュータ システムでは、保留になっている仕事を追跡し続ける手段として債権者のない請求書を使えるものと期待するでしょう)。そうであれば、これは分析で捕捉しているべきもう 1 つの問題点です。
テスト ファースト設計を行うのは、設計の作成者でもそれ以外の人でもかまいませんが、 設計の作成者が行うのが一般的です。その利点は、コミュニケーションによるオーバーヘッドを減らせることです。まず、成果物の設計者とテスト設計者が互いに情報交換する必要がありません。さらに、独立したテスト設計者であれば設計についてよく知るために時間を費やさなければなりませんが、その内容は元の設計者なら既に知っていることです。また、問うべき疑問の多くは、「状態 X でコンプレッサが故障したらどうなるのか」などのように、ソフトウェア成果物の設計時にもテストの設計時にも自然と沸いてくる疑問なので、同一人物が 1 度だけその疑問を出し、その答えをテストのフォームに書いた方が好ましいでしょう。
一方、不利な点もあります。まず、成果物の設計者は、大なり小なり自分の誤りに気付きません。これらの誤りはテスト設計のプロセスでいくらかは判明するでしょうが、おそらく別の人が見つけるほどには見つかりません。これがどの程度の問題になるかは、人によって大きく異なり、多くの場合、設計者の経験の量に関係します。
同一人物がソフトウェア設計とテスト設計の両方を担当する場合のもう 1 つの不利な点として、並行作業が全くないことが挙げられます。別々の人物に役割を割り当てると、投入できる全体の労力が増えるため、全体に要する期間はおそらく短くなります。参加者達が早く設計から実装に移行したくていらいらしている場合などは、テスト設計に要する時間がストレスの原因になります。さらに重大なことには、次の段階に移行するために作業をいい加減にする傾向も出てきます。
この答えは「ノー」です。その理由は、設計時にすべての決定を下してしまうことはできないからです。実装中に下された決定は、設計を基に作成したテストでは十分にテストされません。その典型的な例として、配列を並べ替えるルーチンのケースを説明しましょう。並べ替えには、異なる多くのアルゴリズムがあり、それぞれに長所と短所があります。クイックソートは、通常、大きい配列に対しては挿入ソートより高速ですが、小さい配列に対してはほとんどの場合が挿入ソートより遅くなります。このため、要素数が 15 個より多い配列にはクイックソートを使用し、それ以外の場合は挿入ソートを使用するといった並べ替えアルゴリズムを実装する場合もあります。このような分割は、設計成果物からはわかりません。設計成果物の中で明示しておくことも可能ではありますが、設計者がこのような明確な決定を下すことにそれほどの価値はないと判断する場合もあるでしょう。設計の中では配列のサイズはたいした役割を果たしていないため、テスト設計ではうっかり小さい配列だけを使用することにしてしまうかもしれません。この場合、クイックソートのコードは一切テストされないことになります。
別の例として、次のようなシーケンス図について考えてみましょう。この図では、SecurityManager が StableStore の log() メソッドを呼び出しています。しかし、この場合、log() がエラーを返しており、それによって SecurityManager は Connection.close() を呼び出しています。

図 5: SecurityManager のシーケンス図の例
これは、実装者にとってはよい指示になります。log() がエラーを返したときは、必ず接続を閉じなければならないのです。テストで調べなければならないのは、実装担当者が本当にそれを行っているかどうか、しかも一部のケースではなくすべてのケースで正しく行っているかどうかということです。これを調べるために、テスト設計者は、StableStore.log() の呼び出しをすべて見つけだし、それぞれの呼び出しポイントでエラー処理が行われていることを確認するようにテストを設計しなければなりません。
StableStore.log() を呼び出すすべてのコードを確認できるのに、このようなテストを実行するのはおかしいと思うかもしれません。エラーが正しく処理されているかどうかをチェックするだけではいけないのでしょうか。
おそらく、検査で十分な場合もあるでしょう。しかし、多くの場合、エラー処理コードはエラーの存在自体が違反であるという前提に暗黙的に依存しているため、 エラー処理コードがエラーの原因となりがちなのは周知の事実です。この典型的な例が、割り当てエラーを処理するコードです。次に例を示します。
while (true) { // トップ レベルのイベント ループ
try {
XEvent xe = getEvent();
... // プログラムのメイン部分
} catch (OutOfMemoryError e) {
emergencyRestart();
}
}
このコードは、クリーンアップを行ってから (メモリを利用可能な状態にしてから) イベントの処理を続行することにより、メモリ エラーから回復しようとしています。これは許容できる設計だったと仮定しましょう。メモリが割り当てられない場合の処理は、emergencyRestart が行います。問題は、emergencyRestart が何らかのユーティリティ ルーチンを呼び出すことです。そのユーティリティ ルーチンもまた別のユーティリティ ルーチンを呼び出し、それもさらに別の何らかのユーティリティ ルーチンを呼び出します。そして、呼び出されたユーティリティ ルーチンが新しいオブジェクトにメモリを割り当てます。これを行わなければメモリがないため、結果的にプログラム全体が失敗してしまいます。この種の問題は、検査全体を通じて見つけにくいものです。
ここまでは、可能な限り早いタイミングで、できる限り多くのテスト設計を行うということを前提にして説明してきました。この前提では、設計成果物から引き出せるすべてのテストを引き出し、その後、実装の内部的なことに基づくテストだけを追加します。このような完全なテストは反復の目的とは協調しないため、推敲フェーズでは適切ではない場合もあります。
たとえば、投資家に製品の実現可能性を示すために、アーキテクチャのプロトタイプを作成しようとしているとします。これは、少数の鍵となるユース ケースのインスタンスを基に作成すればよいでしょう。テストでは、コードがそれらをサポートしていることを確認するためにコードをテストしなければなりません。では、さらに詳しいテストを作成すると、何か害があるでしょうか。たとえば、プロトタイプでは重要なエラー ケースを無視していることがはっきりわかっている場合もあります。そのエラー処理を調べるテスト ケースを記述して、そのエラー処理の必要性を記録しておいてもいいのではないでしょうか。
しかし、プロトタイプがやるべき処理を行っているのに、そのアーキテクチャのアプローチがうまく働かないことが判明したとしたらどうでしょうか。この場合、そのアーキテクチャは破棄され、エラー処理のための前述のテストもすべて一緒に破棄されることになります。こうなると、テストの設計に費やした労力は全くの無駄になります。これなら、もっと待って、概念を検証するためのこのプロトタイプが本当に概念を検証できるかどうかをチェックするために必要なテストだけを設計しておいた方がよかったでしょう。
些細なことに見えるかもしれませんが、実際のプロジェクトではこういったことが心理的に非常に大きく影響します。推敲フェーズは、大きなリスクに対処するためのフェーズです。プロジェクト チーム全体がこれらのリスクに集中する必要があります。しかし、担当者達が小さな問題に集中することにより、チームの集中力とエネルギーが削がれてしまいます。
では、推敲フェーズでテスト ファースト設計が効果的に使用できるのはどんな場合でしょうか。アーキテクチャのリスクを十分に調査する作業では、これが重要な役割を果たします。どの場合にリスクが実現化され、どの場合に回避されるかについてチームがどの程度知っているかを正確に考慮すると、設計プロセスがより明快になり、その結果、最初からより優れたアーキテクチャを構築できます。
作成フェーズでは、設計成果物が最終的な形になります。必要なすべてのユース ケースの実現化されたものが実装され、すべてのクラスのインターフェイスも実装されます。このフェーズの目標は完全性なので、完全なテスト ファースト設計が適しています。後の成り行きからテストが無効になることは、たとえあったとしてもごくわずかです。
方向づけフェーズと移行フェーズでは、一般に、テストすることが適切である設計作業にはあまり焦点が置かれません。とはいえ、設計作業に焦点が置かれる場合はテスト ファースト設計を適用できます。たとえば、方向づけフェーズでの候補となる概念の検証作業にはテスト ファースト設計を使用できます。ただし、作成フェーズや推敲フェーズの場合と同じように、反復の目的と協調させる必要があります。
|