テストファーストによるソフトウェア開発の衝撃(前編)

皆さんはテストの本質を理解されていますか? 実は、テストには機能検証をするということ以上に重要な役割があるのです。本稿では、テストファーストがソフトウェアアーキテクチャに及ぼす多大な影響について説明します。

» 2007年02月27日 08時00分 公開
[瀬谷啓介,ITmedia]

テストファーストとは?

 皆さんは「テストファースト」という言葉をどこかで耳にしたことがあるのではないでしょうか? これは単に、「コードを書く前にテストケースを書きなさい」ということであり、この手法をはじめて耳にしたときには何ら驚きを感じないことと思います(私自身そうでした)。

 ソフトウェア開発の経験をある程度積んだエンジニアであれば、テストの重要性は自らの苦い経験から学んでいるはずですし、実装する関数一つ一つに確実にテストが用意されていれば、テスト自身が生きた使用例になるというメリットも頭で理解できるはずです。しかし、「テストファースト」がただテストを用意するだけのことであれば、「テストを後でしっかり用意すれば同じことでは?」と思われるのではないでしょうか? 確かに一見すると、それでも同じように思われます。しかし、ではなぜ「テストファースト」という概念が、業界のエキスパートに驚嘆をもって受け入れられたのでしょう? その秘密は、「テストファースト」は、テストを書く行為というより、むしろ、「設計」に近い行為であるということにあります。

 この意味を真に理解するとき、その深遠な派生効果に大きな衝撃を受けることでしょう。皆さんのソフトウェア開発スタイルは完全に変わってしまうかもしれません。

テストファースト・ソフトウェア開発例――テストファーストは設計行為である

 ここで、単純なアドベンチャーゲームをテストファーストで開発することを考えてみましょう。このゲームは、プレーヤーが各部屋を東西南北に移動し、モンスターを倒しながらゴールにたどり着くという単純なものです(図1)

図1 図1 単純なアドベンチャーゲーム

 テストファーストの教えに従い、まだ何もコードを書いていない状態でテストを書くことにします。まだ何もコードが用意されていないのですから、このテストで使うメソッド(関数)は存在していません。従って、自分はこんな感じにゲームを書くという、意図を持って書くことになります。こういったスタイルでソフトウェアを開発することを、「意図的なプログラミング」と呼びます。ここでは、最初のテストtestMove()を次のように書いてみました。


Public void testMove()
{
    AdventureGame game =
        new AdventureGame();
    game.connectRooms(5,6,"north");
    game.putPlayerRoom(5);
    game.movePlayer("north");
    assertEquals(6, game.findPlayerRoom());
}

 ここではまずゲーム盤を作り、connectRooms()を使って部屋5と部屋6を接続します。そして、まず部屋5にプレーヤーを置き、その直後にプレーヤーを北に移動させます。最後にプレーヤーが部屋6にいるかどうかを確認しています。

 このコードは非常に単純で理解できない部分はないと思いますが、これを見て何か気づかれたでしょうか? このコードにはRoomクラスが登場していません。これは、connectRooms()を使って部屋と部屋を接続するときに、Roomクラスは不要だと判断したからです。

 しかし、Roomクラスが存在しないというのは直感に反するかもしれません。なぜなら、このゲームの性格上、実行されるのはRoomに関する処理ばかりだからです。では、このようにソフトウェアを設計したのは間違いだったのでしょうか? この場では「部屋と部屋を接続するという行為がRoomという概念よりも重要」であるか否かといった議論をするつもりはありません。この例を通して皆さんに気づいていただきたい重要な点は、「テストを書くという行為が、設計上の判断を要求する」という事実です。このテストが、早い段階で設計上重要な問題に光を当てたということが重要なのです。

必然的に導き出される最良のアーキテクチャ――ソフトウェアの分離が自然に促進される

 テストファーストでコードを開発するという行為が、ソフトウェアの分離を促進するという効果も見逃せません。ここでは、英単語の4択問題を出題する、試験対策アプリケーションを考えてみましょう。

 QuizMasterクラスは、Evaluatorオブジェクトに解答が正解か不正解かを判断させ、その結果をQuizオブジェクトに報告します。最終的には、この結果を保持したQuizをデータベースに記録するものとします。

 このアプリケーションのコードはまだ何も書かれていないものと仮定しましょう。つまり、設計に関しては前記のような軽いアイデアを思い描いた段階で、紙切れの片隅に図2を描いている程度だとします。

図2 図2 アプリケーションの設計アイデア

 テストファーストの教えに従い、Quizのテストコードを最初に書かなければならないわけですが、このテストコードを書くには幾つか問題があります。

 データベースに何を使ったら良いのでしょう? このテストを書く前に、まず完全なデータベースを用意しなければならないのでしょうか? EvaluatorやQuizについても完全なものを用意しておかなければならないのでしょうか?

 テストを書く前に、そういったものをすべて完全に用意するのは無理です。そもそも、そういったものを用意するためには、それらのテストコードを書かなければならず、そうなればそのテストコードの中でQuizを使わなければならなくなるでしょう。これでは、鶏が先か卵が先かという議論に陥ってしまいます。

 こういった問題を解決してくれるのが、MockObjectパターンです。これは、Quizと協調するオブジェクトとの間にインタフェースを設け、テスト用のスタブ関数(ダミー関数)でそのインタフェースを実装するというものです。図3にインタフェースを導入した構造を示します。

図3 図3 インタフェースを導入した構造

 次のリストにTestQuizMasterのテストの一部を示します。


public void testPass()
{
QuizDatabase qd = new MockQuizDatabase();
Evaluator eval = new MockEvaluator();
    QuizMaster qm = new QuizMaster(qd, eval);
    qm.generateQuiz(10); //クイズを10題作成
    Quiz quiz = qm.getQuiz(1); //第1問目を取得
    assertTrue(quiz.pass(3)); //3を選択したのであれば正解
}

 ここでは、まず必要なMockオブジェクトを作り、それらを結びつけたQuizMasterを生成し、そこから1問目のQuizを取得するという処理を行っています。そして、最後にその問題の正解が「3」であるかどうかをチェックしています。

 もちろん、このテストがチェックしていることは、QuizMasterが正しいデータを使って正しく関数を呼び出したことだけです。Quizに正しい結果が書き込まれたのかどうかチェックしているわけでなければ、データベースが正しく更新されたかどうかをチェックしているわけでもありません。ここでチェックすべきことは、QuizMasterが単独で正しい動作をしているかどうかということだけだからです。

 このように、QuizMasterが周囲から分離されていると、アーキテクチャが柔軟になります。テストを行ったりシステムを拡張するときに、データベースや評価エンジン(Evaluator)をいつでも別のものと入れ替えられるからです。このような「柔軟なアーキテクチャが、テストをする必要性から導き出されたことに注目」してください。テストを可能にするためには、テスト対象となるモジュールを周囲から分離する必要があり、このことがプログラムの全体構造にとって有益な分離を促進することにつながるわけです。つまり、「テストファーストでコードを開発することは、コードの質を高めることにつながる」というわけです。

後編へつづく

本記事は、UNIX USER 2005年4月号特別企画「テストファーストによるソフトウェア開発の衝撃」を再構成したものです。


Copyright © ITmedia, Inc. All Rights Reserved.

注目のテーマ