レベル: 中級 Eric Allen (eallen@cs.rice.edu), Ph.D. candidate, Java programming languages team, Rice University
2001年 9月 01日 Javaコードの診断 連載の今回の記事で、Eric Allen氏は、特定のバグ・パターンの調査を一時中断し、代わりに、テストするのが容易で面白いとも思えるソフトウェアを設計するのに関係する事項を、検討することにしました。同氏は、テストを記述する際に生産性を大幅に向上でき、その結果コード・ベースを強化する7つの設計原則について概要を示しています。
大規模なプログラムを設計する際には、常に留意しておかなければならないことがあります。それは、設計面での選択が、パフォーマンスや拡張性などの特性に与える影響です。ソフトウェア製品がますます複雑になり、いろいろなところに配置されるようになると、ソフトウェアの「テスト可能性」は、一層重要な考慮事項になります。
コードを十分にテストすることが重要なことは言を待ちません。テスト・ケースを作成したり、コードをテストしたりすることに費やされる時間や労力は、保守費用が大幅に削減されることによって充分回収できます。
しかしながら、注意しないと、コードのテストにかかる労力は、最初にコードを書く労力の何倍にも及ぶことがあります。プログラマー達が、単体テストでコードをすべてカバーするために協調してテスト・ケースを洗い出したところ、膨大な時間がかかることが分かるとすっかり気落ちしてしまう場面を、私はよく目にしてきました。
しかし、幸い、ここに示すテストでは、そのようなことにはなりません。ソフトウェアを設計する際に基本的な原則をいくつか適用すれば、テストを容易に、かつ楽しく行えるようなコードを書くことができます。
他のコーディング原則の場合と同様、以下に述べる方法は、疑問の余地のない、あるいは変わることのない定理ではありません。この規則を破らなければならない場合も多々あります。 このため、各原則の背後にある真の目的を理解し、その目的に当てはまらない (または、より重要な関心事が優先する) 場合を判別できるようになることが重要です。
原則1. GUIビューの外側に
できるだけ多くのコードをGUIのビューの外へ移してください。そうすれば、様々なGUI動作を、モデル上の単純なメソッド起動にしてしまうことができます。この理由は次のとおりです。
- GUIテスターを用いて間接的にテストするより、メソッド起動を通して機能性をテストするほうが容易です。
- さらに、ビューに影響を与えずにプログラムの機能性を容易に修正できるという利点もあります。
もちろん、ビューに関しても、バグはあり得ます。理想的には、プログラムのテストによって、モデルとビューの両方を検査します。(ビューのテストに関しては、私の記事「うそつきビューのバグ・パターン」(the Liar View bug pattern) またはJeffries氏その他の「Extreme Programming Installed」を参照してください。両記事とも、参考文献の項にリンクされています。)
原則2. 型によるエラー検査
型は、友人のようなものです。できるだけ自動的にエラーを検査するために型システムを使用してください。
型は、プログラムを実行する前に、プログラムのバグを自動的に捕らえることができます。静的な型チェックをしないと、正しい実行パスが型エラーを運良く発見するまで、型エラーは、プログラム内に妨害者として残ります。
型を最大限に利用するのは、かなり手際のいる仕事です。しばしば、ある一群のデータ構造は、ある抽象レベルで共に使用されるか、または、新たな関連するデータ型によって、単一でより高位の抽象レベルを共通因子としてくくるだされることもあります。
実際、プログラミング言語自身の歴史は、プログラムする抽象レベルを徐々に高めることであったとみることができます。アセンブリー言語は、単なるビットを整数および浮動小数点へ抽象化しました。その後、レコードや関数などの抽象化が続き、次いでオブジェクト、クラス、スレッド、例外が登場しました。
各抽象レベルにおいて、より高い抽象レベルと同じ機能性が達成可能ですが、より一層の労力と、過ちを犯すというリスクを伴えばの話です。
オブジェクト指向言語では、(他の近代的な言語と同様) 抽象化を試みる際、個々のプログラマーに大きな柔軟性が与えられています。そして、どの抽象レベルでプログラムを設計すべきかは、トレードオフに基づく判断を求めるようになりました。たとえば、抽象レベルによって得られる堅牢さと、より低い抽象レベルで作業しないことで失われる表現能力 (および、時としてパフォーマンス) との間のトレードオフなどです。
一般に、高位の抽象レベルによってもたらされる堅牢さと単純さが、他の考慮事項より優先されることはめったにありません。(この問題の詳しい議論は、参考文献 の項のImpostor Type bug patternを参照してください。)
原則3. 「断層線」を避けるために仲介機能(mediator)を利用する
私の言う「断層線」というのは、独立したコンポーネント間のインターフェースであって、コンポーネント内部のサブコンポーネント間の対話に比べて、ほとんど対話のないインターフェースを指します。このような断層線の典型的な例は、GUIビューとそのモデル間のインターフェースです。他にも、コンパイラーの様々な処理フェーズ間のインターフェースや、オペレーティング・システムにおけるカーネルとユーザー・インターフェース間のインターフェースなどがあります。
プログラム内の断層線を探し、集約コンポーネントに迅速にアクセスする機能をもつ仲介機能を利用してください。
断層線に沿って各コンポーネントを分離してテストするほうが容易なことも、しばしばあります。しかし、各コンポーネントによって外に開示されるオブジェクトが多い場合、またはコンポーネント内のテストしたいオブジェクトのいくつかが、何段かにネストされた参照によってしかアクセスできない場合、テストはまったく面倒なものになる可能性があります。
分離してテストするのではなく、テストしたいさまざまなメソッドを呼び出すことができる1つの仲介オブジェクトを持つと役に立つことがよくあります。このオブジェクトは、これらのメソッド呼び出しを適切な場所に送ることができます。
同様に、プログラム・コンポーネントへのインターフェースをそれらへのテストと対に設計するのも有効です。こうしておくと、ユーザーはこれらインターフェースをできるだけ簡素にしておくことに努力を集中します。
原則4. メソッド 小さなシグニチャーおよびデフォルトの引数
小さなメソッド・シグニチャーや、デフォルトのメソッド引数を持つ多重定義メソッドを使用すれば、テストでこれらのメソッドを起動するのが大変楽になります。さもないと、メソッドをテストする際、追加の引数を構成する必要が生じます。引数の数が多いと、すぐにコードが増えてしまいます。この場合さらに悪いことには、本来作成しようとしていたテストの数をより少なくしがちになります。
原則5. アクセサーは、メモリー状態を修正してはいけない
テストでオブジェクトの状態を検査するには、メモリー状態を変更しないアクセサーを使用してください。
いくつかの点で、テストは、研究室の実験に似ています。テストでも実験でも、特定の仮説が正しいかどうかを証明しようとします。検査することにより、検査対象の状態が変わる場合には、これはさらに困難になります。
量子力学の世界とは異なり、コンピューター処理における状態は、その状態を変更せずに検査できます。この利点を生かしてください。
原則6. インターフェースを使ってプログラム外のコンポーネントを明示する
プログラム外のコンポーネントを明示するインターフェースを使用すると、テスト・ケースで、このようなコンポーネントのシミュレーションを容易に行えます。
この原則により、特に外側のコンポーネントの実装が不完全な場合、時間を大幅に節約できます。しばしば、ほとんどの重要なコンポーネントは、期限どおりには出来上がりません。これらのコンポーネントが整わないためにコードをテストができなければ、悲劇です。2週間遅れているコンポーネントを統合するのに2時間しか掛けられないという事態であったとしても、それはクライアントが気にしたことではありません。クライアントに分かることは、統合された製品の納期が遅れて、しかもそれが機能しない、ということだけです。
原則7. 最初にテストから考える
まずテストを作成してください。これは、標準的なXP手順ですが、無視されがちです。
私は、この手順を無視するという誘惑に負けるたびに、いつも後悔しています。正しいコードを作成しようとする場合、テストの記述を延期すれば、時間が節約できるように見えるというのは、幻想でしかありません。
注: これは、実装全体を行う前に、テスト全体を一発で作成しなければばならないということではありません。テストを2つ3つ作成し、それらを実装し、またいくつかのテストを作成して実装する、という方法が良いでしょう。このようにして設計を進めれば、実装の段階で誤りを見つけ、その次のテスト・セットでその誤りを訂正できます。また、このようにすればテストを書くほうが気楽です。
必要以上のコード?
どのようなプログラムでも、少し労力を掛ければ、徹底的にかつ容易にテストできます。もちろん、これらの原則が当てはまらないことも当然あるでしょうが。この場合機能性はテストできないように見えます。
こうなった場合、私なら、「どうすれば、この種のコードをテストできるだろうか?」という疑問から一歩身を引いてみます。そして、この疑問に代えて、「どうしたら、テストできる方法でこのコードを書けるか?」と自分自身にしばしば、テストを容易にするという目的だけに寄与する機能を多く追加することになります。
何ですって? 心配はいりません。そうなっても、すべてうまくいきます。
既存のデザイン・パターンの多くが、もっぱら拡張性をプログラムに追加するためにだけに必要な複数のクラス (visitors、decoratorsのような) をプログラムに追加しています。これと同様に、テストを容易にするためには新たなパターンを開発することは容認できます。実際、あるオブジェクト指向言語の多くの機能は、拡張性を容易にするために提供されていますが、その言語の将来のバージョン (またはまったく新しい言語) に、容易にテストを行える機能を含めない理由はありません。
Java言語の場合は、既にこれが始まっています。将来のバージョンでは、さらに強力な型システム、表明などを組み込むことが予定されています。オブジェクト指向言語においては、既存のコードを再利用ないし拡張できる割合が増えたように、将来はテスト指向設計および機能が、古いコードと新しいコードの両方を一層堅牢なものにする役割を果たすでしょう。
参考文献
著者について  | |  | Eric Allen氏は、コーネル大学でコンピューター・サイエンスおよび数学を専攻し、A.B.(Bachelor of Arts)を取得しました、また現在は、ライス大学、Javaプログラミング言語チームの博士課程に在籍しています。彼の研究対象は、Java言語のソース / バイトコード・レベルでのセマンティック・モデルおよびスタティック分析ツールの開発です。また現在は、NextGenプログラミング言語 (ジェネリック・ランタイム・タイプを持つJavaの拡張言語) のためのソース・ツー・バイトコード・コンパイラーを作成しています。彼の連絡先は、eallen@cs.rice.edu です。 |
記事の評価
|