レベル: 初級 Andrew Glover (aglover@stelligent.com), CTO, Stelligent Incorporated
2006年 10月 31日 開発者によるテストが重要であることは誰もが認めていますが、テストの実行にはどうしてそんなに時間がかかるのでしょうか。今月の記事では、Andrew Glover がエンド・ツー・エンドのシステムの安定性を確実にするために必要な 3 つのテスト・カテゴリーにスポットライトを当て、テストを自動でカテゴリー別に分類して実行する方法を紹介します。これにより、今日の膨大なテスト・スイートでさえもビルド時間が劇的に短縮されることになります。
想像してみてください。時は 2002 年の初め、あなたは新興企業で働く開発者です。景気は良好で、あなたは同僚とともに最新最強の Java™ API を使用した大規模なデータ駆動型 Web アプリケーションをビルドする任務を負っています。あなたと経営陣は、結果的にアジャイル・プロセスとして知られるものの信奉者です。初日から Ant ビルド・プロセスの一環として、JUnit で作成したテストを可能な限り何度も実行しています。ついには、ビルドを夜通し実行するために cron ジョブをセットアップしました。後日、他の誰かが CruiseControl をダウンロードして、チェックインのたびに巨大化したテスト・スイートを実行することになります。
ここで、時代を現代に移します。
過去数年間にわたり、あなたの会社は膨大なコード・ベースを同じく膨大な JUnit テスト・スイートで開発しました。すべて順調だったのは一年前までで、その頃、テスト・スイートはテスト数 2,000 を上回り、ビルド・プロセスを実行するのに 3 時間以上かかることに対する懸念が持ち上がったのです。その数ヶ月前、継続的インテグレーション (CI) によりコードのチェックイン時にユニット・テストを実行することはやめることにしました。CI サーバーに負担がかかるからです。その代わりテストは夜間に実行することにしたため、毎朝、開発者たちは何が何故壊れたのかを判明する作業に追われています。
最近では、テスト・スイートはほとんど夜間に一度しか実行されなくなってきています。それは何故かといえば、大変な時間がかかるからです。万事が順調に機能している (あるいは機能していない) ことを確認するだけのために数時間もじっと待つような人はいません。しかもテスト・スイートは夜中に実行されるのです。
テストはまれにしか実行されないため、エラーが頻繁に伴うようになりました。その結果、開発者チームはユニット・テストの価値に疑問を抱き始めています。ユニット・テストはコード品質にそれほど重要なことなのか、どうしてこんなにやっかいなのか...。ユニット・テストをもっと短時間で実行できない限り、誰もそこに基本的な価値があるとは納得できません。
テストのカテゴリー化を試してみてください
ここで必要なのは、ビルドをより迅速なものに変えるための戦略です。つまり、テストを毎日一回以上実行し、ビルドが完了するまでに 3 時間もかかるようになる前の状態にテスト・スイートを戻すためのソリューションが必要です。
テスト・スイート全体を立て直すための戦略を考え出す前に、「ユニット・テスト」という総称についてもう一度考えてみてください。「家には動物がいます」とか、「車が好きです」などという言い方があまり具体的でないのと同様に、残念ながら「ユニット・テストを作成する」という文句も曖昧です。最近では、ユニット・テストはどんな意味にもとれます。
上記の動物と車についての発言を例に挙げると、このような発言は多くの質問につながります。例えば、どんな種類の動物が家にいるのか、それは猫か、トカゲか、熊か、といった具合です。「家には熊がいます」と言えば、「家には猫がいます」とは明らかに意味が異なります。同様に、「車が好きです」と言っても自動車のセールスマンとの会話ではあまり役に立ちません。好きな車のタイプがスポーツ・カー、トラック、あるいはステーション・ワゴンのどれかによって、会話が進む道は異なってきます。
開発者のテストの場合も同じで、テストをタイプ別にカテゴリー化すると話が早くなります。テストをカテゴリー化すれば、言葉の表す内容がより具体的になり、チームが異なるテスト・タイプを異なる頻度で実行できるようになります。カテゴリー化は、すべての「ユニット・テスト」を実行する、あの気の重い 3 時間のビルドを回避する鍵となるのです。
3 つのカテゴリー
テスト・スイートを 3 つのレイヤーに分けてみてください。それぞれのレイヤーが表す開発者テストのタイプは、そのテストの実行時間によって定義されます。図 1 に示すように、各レイヤーは実行時間または作成時間のいずれかを合計ビルド時間に加算します。
図 1. テスト・カテゴリー化の 3 つのレイヤー
一番下のレイヤーは実行時間が最短のテストで構成されます。ご想像のとおり、作成の難易度はもっとも低く、扱うコードの量も最小のテストです。一方、一番上のレイヤーを構成するのはより上位レベルのテストで、アプリケーションの大部分を実行します。これらのテストはコード作成の難易度も高く、実行時間も大幅に増えます。中間のレイヤーは、この 2 つのレイヤーの間に分類されるテストを対象としています。
この 3 つのカテゴリーは以下のとおりです。
- ユニット・テスト
- コンポーネント・テスト
- システム・テスト
それでは、これからそれぞれのカテゴリーについて検討してみましょう。
1. ユニット・テスト
ユニット・テストは 1 つ以上のオブジェクトを単独で検証します。ユニット・テストは、データベースやファイル・システム、そしてテストの実行時間を長くさせるようなものを一切扱わないため、初日から作成できます。まさにこの目的で設計されたのが、JUnit です。無数の擬似オブジェクト・ライブラリーの背後にはユニット・テストの分離の概念があり、これによって特定のオブジェクトを外部依存関係から簡単に切り分けられるようになっています。さらに、ユニット・テストは実際のテスト用コードを書き込む前に作成できます。つまり、テスト先行型開発の概念です。
ユニット・テストはアーキテクチャーの依存関係とは独立しているため、一般的にそのコードは作成しやすく、実行するのにも時間がかかりません。欠点としては、個別のユニット・テストのコード・カバレッジが若干制限されるということです。ユニット・テストの (非常に大きな) 価値は、開発者が考えられる限りの最下位レベルでオブジェクトの信頼性を保証できることにあります。
ユニット・テストは実行時間も短く、作成するのも簡単なため、コード・ベースには多数のユニット・テストを作成してできるだけ頻繁に実行するようにしなければなりません。マシン上であろうと、CI 環境のコンテキスト内であろうと、ビルドの実行時には必ずユニット・テストを実行してください (つまり、コードを SCM システムにチェックインするときには常にユニット・テストを実行するということです)。
2. コンポーネント・テスト
コンポーネント・テストは、相互作用する複数のオブジェクトを検証しますが、分離の概念に反しています。コンポーネント・テストはアーキテクチャーの複数のレイヤーを対象としているため、データベース、ファイル・システム、ネットワーク要素などを扱うことも珍しくありません。コンポーネント・テストは初期の段階で作成するのは若干難しいため、完全なテスト先行型/テスト駆動型のシナリオに組み込むのはかなりの難題です。
コンポーネント・テストはユニット・テストよりも複雑なので、作成にも時間がかかります。一方、コンポーネント・テストの範囲は広いため、コード・カバレッジはユニット・テストよりも向上します。同時に実行時間も長くなるため、多数のコンポーネント・テストをまとめて実行すると全体的なテスト時間が大幅に増加します。
大規模なアーキテクチャー・コンポーネントのテストに関連する課題は、多数のフレームワークによって軽減されます。そのようなフレームワークの絶好の例は、DbUnit です。DbUnit は、テスト前後のデータベースのシードに関する複雑さをうまく処理することで、データベースに依存するテストの作成を簡単に行えるようにします。
ビルドのテスト時間が長引く場合、大規模なコンポーネント・テスト・スイートが関わっていると言ってほとんど間違いありません。コンポーネント・テストはユニット・テストより実行に時間がかかるため、常時実行することはできないと判断することもあります。したがって、CI 環境でのコンポーネント・テストは 1 時間に 1 回の程度で実行するのが妥当です。コンポーネント・テストは、コードをチェックインする前に必ずローカル開発者のボックスで実行してください。
 |
受け入れテストについてはどうでしょう?
受け入れテストは機能テストと似ていますが、理想としてはカスタマーまたはエンド・ユーザーが作成するという点が異なります。機能テストと同じく、受け入れテストはエンド・ユーザーの方法で行います。受け入れテストのフレームワークとして大きな注目を集めているのが、ブラウザーを使用して Web アプリケーションをテストする Selenium (「参考文献」を参照) です。JUnit テストと同様、Selenium もビルド・プロセスで自動化できます。ただし、Selenium は新しいプラットフォームなので、JUnit を使用することも、それと同じように動作することもありません。 |
|
3. システム・テスト
システム・テストは、ソフトウェア・アプリケーションをエンド・ツー・エンドで検証するため、アーキテクチャーの複雑性が高くなります。システム・テストを実行するには、アプリケーション全体を実行しなければなりません。例えば Web アプリケーションでシステム・テストを実行する場合、データベース、そして Web サーバー、コンテナー、その他すべての関連する構成要素にアクセスする必要があります。そのため、ほとんどのシステム・テストは、ソフトウェア・ライフ・サイクルの後のほうのサイクルで作成されます。
システム・テストの作成は困難で、実際の実行にもかなりの時間がかかりますが、いわゆるアーキテクチャーのコード・カバレッジの点でその苦労に見合うだけの価値があります。
システム・テストは機能テストと非常によく似ています。その違いは、システム・テストではユーザーをエミュレートするのではなく模倣するという点です。コンポーネント・テストの場合と同じく、システム・テストを手助けする多数のフレームワークが作成されています。例えば、jWebUnit はブラウザーを模倣することによって Web アプリケーションのテストを円滑に行えるようにします。
 |
jWebUnit と Selenium のどちらを使うべきか
jWebUnit はシステム・テストのために設計された JUnit の拡張フレームワークなので、テストの作成が必要になります。Selenium は受け入れテストと機能テストに優れたフレームワークで、jWebUnit とは違ってプログラマーでなくてもテストを作成できます。チームが両方のツールを使用してアプリケーションの機能を検証することが理想です。 |
|
テスト・カテゴリー化のインプリメンテーション
ユニット・テスト・スイートとは、実際はユニット・テスト、コンポーネント・テスト、そしてシステム・テストのスイートであることがわかりました。さらに、テストを調べてみて、ビルドの大多数がコンポーネント・テストであるために、3 時間もかかっていることがわかりました。次に出てくる質問は、JUnit でテストのカテゴリー化をインプリメントするにはどうしたらいいか、ということでしょう。
選択肢はいくつかありますが、この記事では、もっとも簡単な以下の 2 つの方法を取り上げます。
- 目的のカテゴリーに対応するカスタム JUnit スイート・ファイルを作成する。
- テストのタイプごとにカスタム・ディレクトリーを作成する。
 |
TestNG によるテストのカテゴリー化
TestNG では、非常に簡単にテストのカテゴリー化をインプリメントできます。TestNG の group 注釈を使うと、目的のテストに適切な group 注釈を適用するのと同じくらい簡単に、テストを論理的にカテゴリー化できます。これにより、対応するグループ名を Ant などのテスト・ランナーに渡すだけで、特定のカテゴリーを実行できます。 |
|
カスタム・スイートの作成方法
一連のテストをグループとして定義するには、JUnit の TestSuite クラス (タイプは Test) を使用します。まず、TestSuite のインスタンスを作成して、対応するテスト・クラスまたはテスト・メソッドを追加します。次に、suite() という名前の public static メソッドを定義して、JUnit にこの TestSuite インスタンスを指定します。これにより、このインスタンスに含まれるすべてのテストが単一のものとして実行されるようになります。このように、ユニットの TestSuite、 コンポーネントの TestSuite、そしてシステムの TestSuite を作成することによって、テスト・カテゴリー化をインプリメントできます。
例えばリスト 1 に示すクラスでは、suite() メソッドにすべてのコンポーネント・テストが保持された TestSuite を作成しています。このクラスは JUnit 固有のものではないことに注意してください。そのため、クラスが TestCase を拡張することも、クラスにテスト・ケースが定義されることもありませんが、JUnit はリフレクションによって suite() メソッドを検出し、このメソッドが戻すすべてのテストを実行します。
リスト 1. コンポーネント・テストの TestSuite
package test.org.acme.widget;
import junit.framework.Test;
import junit.framework.TestSuite;
import test.org.acme.widget.*;
public class ComponentTestSuite {
public static void main(String[] args) {
junit.textui.TestRunner.run(ComponentTestSuite.suite());
}
public static Test suite(){
TestSuite suite = new TestSuite();
suite.addTestSuite(DefaultSpringWidgetDAOImplTest.class);
suite.addTestSuite(WidgetDAOImplLoadTest.class);
...
suite.addTestSuite(WidgetReportTest.class);
return suite;
}
} |
TestSuite の定義プロセスでは、既存のテストを調べて、それぞれを対応するクラスに追加しなければなりません (つまり、すべてのユニット・テストは UnitTestSuite 内に追加します)。また、特定カテゴリーに新しいテストを作成したら、該当する TestSuite にこれらのテストをプログラマチックに追加し、そして当然、再コンパイルする必要があります。
個別 TestSuite の実行は、正しいテストの集合を呼び出す固有の Ant タスクを作成する練習になります。ComponentTestSuite を選択する component-test タスクなどを定義するには、リスト 2 のようにします。
リスト 2. コンポーネント・タスクのみを実行する Ant タスク
<target name="component-test"
if="Junit.present"
depends="junit-present,compile-tests">
<mkdir dir="${testreportdir}"/>
<junit dir="./" failureproperty="test.failure"
printSummary="yes"
fork="true" haltonerror="true">
<sysproperty key="basedir" value="."/>
<formatter type="xml"/>
<formatter usefile="false" type="plain"/>
<classpath>
<path refid="build.classpath"/>
<pathelement path="${testclassesdir}"/>
<pathelement path="${classesdir}"/>
</classpath>
<batchtest todir="${testreportdir}">
<fileset dir="test">
<include name="**/ComponentTestSuite.java"/>
</fileset>
</batchtest>
</junit>
</target> |
できれば、ユニット・テストを呼び出すタスク、そしてシステム・テストを呼び出すタスクも作成してください。テスト・スイート全体を実行する場合には、リスト 3 のように、3 つすべてのテスト・カテゴリーに依存する 4 番目のタスクを作成します。
リスト 3. すべてのテストを実行する Ant タスク
<target name="test-all" depends="unit-test,component-test,system-test"/> |
カスタム TestSuites を作成すれば、テストのカテゴリー化を簡単にインプリメントできます。ただし、この方法には、新しいテストを作成するたびに適切な TestSuite にプログラマチックに追加しなければならないという欠点があり、面倒です。これより拡張性に優れているのが、テストのタイプごとにカスタム・ディレクトリーを作成するという方法です。この方法では、再コンパイルの必要なく、新しくカテゴリー化したテストを追加できます。
カスタム・ディレクトリーの作成方法
JUnit でテストのカテゴリー化をインプリメントする場合、私がもっとも簡単な方法だと思うのは、テスト・タイプに応じた特定のディレクトリーにテストを論理的に分割することです。この手法を使えば、すべてのユニット・テストは unit ディレクトリーに常駐し、すべてのコンポーネント・テストは component ディレクトリーに常駐するといった具合に、タイプごとのディレクトリーにテストを置くことができます。
例えば、カテゴリー化されていないすべてのテストが置かれる test ディレクトリー内には、リスト 4 に示すような 3 つの新しいサブディレクトリーを作成できます。
リスト 4. テスト・カテゴリーをインプリメントするディレクトリー構造
acme-proj/
test/
unit/
component/
system/
conf/ |
これらのテストを実行するには、ユニット・テスト用、コンポーネント・テスト用など、少なくとも 4 つの Ant タスクを定義する必要があります。4 番目のタスクは、3 つすべてのテスト・タイプを実行する便利なタスクです (リスト 3 に示したようなタスク)。
この JUnit タスクはリスト 2 に定義したタスクと非常によく似ていますが、タスクの batchtest 要素に含まれる詳細が異なります。この場合は、fileset が特定のディレクトリーを指定するためです。例えばリスト 5 では、unit ディレクトリーが指定されています。
リスト 5. すべてのユニット・テストを実行する JUnit タスクの batchtest 要素
<batchtest todir="${testreportdir}">
<fileset dir="test/unit">
<include name="**/**Test.java"/>
</fileset>
</batchtest> |
上記のテストは、test/unit ディレクトリー内にあるテストのみを実行します。新しいユニット・テスト (または該当するその他のテスト) を作成する際には、このディレクトリーにテストをドロップするだけで準備万端になります。TestSuite ファイルに新しい行を追加して再コンパイルしなければならない方法より、ずっと簡単です。
問題はこれで解決です
最初のシナリオに戻りましょう。あなたのチームでは、ビルド時間の問題を解決するには、特定のディレクトリーを使用するのがもっとも拡張可能な方法であるという結論に達しました。このタスクで一番難しいのは、テスト・タイプを調べて配分することです。チームは、Ant ビルド・ファイルをリファクタリングして、4 つの新しいタスク (テスト・タイプごとの 3 つのタスクと、すべてのテスト・タイプを実行するための 1 つのタスク) を作成しました。さらに、CruiseControl を変更して、チェックインでは真のユニット・テストだけを実行し、1 時間ごとにコンポーネント・テストを実行するようにしました。さらに検討してみると、システム・テストも 1 時間ごとに実行できることが分かったため、コンポーネント・テストとシステム・テストを一緒に実行するもう一つのタスクを作成することにしました。
最終的な結果として、テストは毎日何度も実行されるようになったため、チームはインテグレーション・エラーを一層素早く (たいていは 2、3 時間以内に) 見つけられます。
ビルド時間の短縮化は流行の最先端というわけではありませんが、コードの品質を確実にする上では非常に重要な役割を果します。テストの実行頻度が増えれば、開発者テストの価値に関する懸念はもう過去の思い出です。しかも 2006 年の今、あなたの会社は大成功を収めることになります。
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について  | 
|  | Andrew Gloverは合衆国ワシントン特別区にある、Vanward TechnologiesのCTO(最高技術責任者)です。Vanward Technologiesは自動化テスト・フレームワークの構築を専門としており、ソフトウェアのバグ発生数や統合時間やテスト時間の減少、また全体的なコード安定性改善に貢献しています。
|
記事の評価
|