レベル: 中級 Elliotte Rusty Harold (elharo@metalab.unc.edu), Adjunct Professor, Polytechnic University
2005年 09月 13日 JUnitは、Java™;言語用のユニット・テスト・ライブラリーのデファクト標準です。JUnit 4は、このライブラリーにとって、過去約3年間で最初の重要リリースです。JUnit 4では、テストの識別に関してサブクラス化やリフレクション(reflection)、命名規則(naming convention)などに頼るのではなく、Java 5の注釈機能を利用しており、それによってテストが単純化されると言われています。この記事では、コード・テストの偏執狂、Elliotte HaroldがJUnit 4を取り上げ、この新しいフレームワークを皆さんの作業の中でどのように使用するかについて解説します。この記事は、既にJUnitに経験のある読者を対象にしています。
JUnitはKent BeckとErich Gammaによって開発されたものですが、これまで開発されたサードパーティーのJavaライブラリーの中で、最も重要なものであることは疑いないでしょう。Martin Fowlerはかつて、「ソフトウェア開発の世界で、これほど僅かのコード行で済ませられることに対して、これほど多くの人に感謝されているものは他に無いでしょう」と言っています。JUnitによって急激にテストが始まり、爆発的なテストの増加につながったのでした。JUnitのおかげで、それまでに比べてJavaコードはずっと堅牢になり、信頼性が高まり、またバグ無しになりました。JUnit(SmalltalkのSUnitをもじったものです)から、様々なxUnitツール・ファミリーができ、それによって広範な種類の言語にユニット・テストの恩恵が広まりました。つまり、nUnit (.NET)、pyUnit (Python)、CppUnit (C++)、dUnit (Delphi)などが、様々なプラットフォームや言語の世界で、無数のプログラマーをテスト病に感染させたのです。
しかし、JUnitは単なるテストにすぎません。JUnitによる実際の恩恵は、フレームワーク自体にあるわけではなく、JUnitの中に具現化されている考え方や手法の中にあります。ユニット・テストや、テスト優先プログラミング(test-first programming)、テスト主導型開発などは、必ずしもJUnitで行う必要はありません。これは、GUIプログラミングは必ずしもSwingで行う必要がないのと同じことです。JUnitそのものが最後に更新されたのは、約3年前です。確かにJUnitは他の大部分のフレームワークよりも堅牢なことが知られており、また長く続いていますが、やはりバグは見つかっています。さらに重要なこととして、Javaが進歩しているのです。Javaは今や、ジェネリックスや列挙、可変長引数リスト、そして注釈をサポートしており、こうした機能によって、再利用可能フレームワークの設計に新しい可能性が生まれています。
JUnitの進行停止は、王座としてのJUnitの地位を奪おうとするプログラマーの格好の標的となっていました。挑戦者としては、Bill VennersによるArtima SuiteRunnerからCedric BeustによるTestNGまで、様々なものがあります。これらのライブラリーは、推薦に値すべき機能を幾つか持っていますが、どれもJUnitが達成しているほどの精神や市場シェアには到達していません。どれも、そのままの形では、AntやMaven、Eclipseのような幅広い製品をサポートする機能は持っていません。そこでBeckとGammaは、Java 5の新機能(特に注釈)を活用して、オリジナル版のJUnitよりもユニット・テストをさらに単純化できるような、新しいバージョンのJUnitを作る作業に取りかかりました。Beckによると、「JUnit 4のテーマは、さらにJUnitを単純化することによって、より多くの開発者が、より多くのテストを書くように仕向けること」です。JUnit 4は既存のJUnit 3.8テスト・スイートと後方互換性を維持していますが、Javaのユニット・テストにおいて、JUnit 1.0以来最も重要な革新となるはずです。
注意: このフレームワークで行われている変更は、未完成な部分が数多くあります。JUnit 4の概要は明確になっていますが、詳細はまだ変更される可能性があります。この記事はJUnit 4に関する暫定報告であって、最終報告ではありません。
テスト方法
JUnitのこれまでのバージョンではすべて、テストを見つけるために命名規則とリフレクションを使っています。例えば下記のコードは、1+1が2であることをテストしています。
import junit.framework.TestCase;
public class AdditionTest extends TestCase {
private int x = 1;
private int y = 1;
public void testAddition() {
int z = x + y;
assertEquals(2, z);
}
} |
JUnit 4は、これとは対照的に、テストは@Test注釈で識別されます。これを下記に示します。
import org.junit.Test;
import junit.framework.TestCase;
public class AdditionTest extends TestCase {
private int x = 1;
private int y = 1;
@Test public void testAddition() {
int z = x + y;
assertEquals(2, z);
}
} |
注釈を使う利点は、testFoo()やtestBar()などのメソッドすべてに名前をつける必要がなくなることです。例えば、次のような手法でも動作するのです。
import org.junit.Test;
import junit.framework.TestCase;
public class AdditionTest extends TestCase {
private int x = 1;
private int y = 1;
@Test public void additionTest() {
int z = x + y;
assertEquals(2, z);
}
}
|
また、こうした方法でも動作します。
import org.junit.Test;
import junit.framework.TestCase;
public class AdditionTest extends TestCase {
private int x = 1;
private int y = 1;
@Test public void addition() {
int z = x + y;
assertEquals(2, z);
}
}
|
これによって、自分のアプリケーションに最も適切な命名規則に従えるようになります。例えば、私がこれまでに見た幾つかのサンプルでは、テスト・クラスがテスト・メソッドの名前に、テスト対象クラスと同じ名前を使う命名規則を使っています。例えば、List.contains()はListTest.contains()によってテストされ、List.add()はListTest.addAll()によってテストされ、などです。
TestCaseクラスは相変わらず動作しますが、これを拡張することは要求されません。テスト・メソッドに@Testと注釈付けさえすれば、任意のクラスに、そのテスト・メソッドを入れられるのです。ただし、様々なassertメソッドにアクセスするためには、junit.Assertクラスをインポートする必要があります。これを次に示します。
import org.junit.Assert;
public class AdditionTest {
private int x = 1;
private int y = 1;
@Test public void addition() {
int z = x + y;
Assert.assertEquals(2, z);
}
} |
また、JDK 5での新しい静的インポート機能を使えば、これを古いバージョンの場合と同じくらい単純にすることができます。
import static org.junit.Assert.assertEquals;
public class AdditionTest {
private int x = 1;
private int y = 1;
@Test public void addition() {
int z = x + y;
assertEquals(2, z);
}
} |
この方法によると、保護されたメソッドを含むクラスをテストケース・クラスが拡張できるため、メソッドをテストから守るのが容易になります。
SetUpとTearDown
JUnit 3テスト・ランナーは、各テストを実行する前に自動的にsetUp()メソッドを呼び出します。このメソッドは通常、フィールドを初期化し、ログをオンし、環境変数をリセットし、などを行います。例えば下記は、XOMのXSLTransformTestのsetUp()メソッドです。
protected void setUp() {
System.setErr(new PrintStream(new ByteArrayOutputStream()));
inputDir = new File("data");
inputDir = new File(inputDir, "xslt");
inputDir = new File(inputDir, "input");
} |
JUnit 4でも、各テスト・メソッドを実行する前にフィールドを初期化でき、環境を設定できます。しかし、それを行うメソッドをsetUp()と呼ぶ必要はもうありません。ただ単に@Before注釈を付けた名前であれば良いのです。これを下記に示します。
@Before protected void initialize() {
System.setErr(new PrintStream(new ByteArrayOutputStream()));
inputDir = new File("data");
inputDir = new File(inputDir, "xslt");
inputDir = new File(inputDir, "input");
} |
さらに、複数のメソッドに@Before注記を付け、それぞれを各テストの前に実行するようにすることもできます。
@Before protected void findTestDataDirectory() {
inputDir = new File("data");
inputDir = new File(inputDir, "xslt");
inputDir = new File(inputDir, "input");
}
@Before protected void redirectStderr() {
System.setErr(new PrintStream(new ByteArrayOutputStream()));
}
|
クリーンアップも同様です。JUnit 3では、次のようなtearDown()メソッドを使います(これはXOMで、大量のメモリーを消費するテストを行うような場合に使います)。
protected void tearDown() {
doc = null;
System.gc();
} |
JUnit 4では、より自然な名前をつけられ、それに@Afterという注釈を付けられるのです。
@After protected void disposeDocument() {
doc = null;
System.gc();
} |
@Beforeの場合と同様、複数のクリーンアップ・メソッドに@After注釈を付け、それぞれを各テストの後に実行するようにすることもできます。
最後に、スーパークラスにある初期化やクリーンアップのメソッドを明示的に呼ぶ必要は、もうありません。これらのメソッドがオーバーライドされていない限り、テスト・ランナーは必要に応じて、これらを自動的に呼んでくれるのです。スーパークラスにある@Beforeメソッドは、サブクラスにある@Beforeメソッドよりも前に呼び出されます。(これはコンストラクター呼び出しの順序と反対です。)@Afterメソッドの実行は、これとは逆です。サブクラスのメソッドはスーパークラスのメソッドの前に呼び出されます。そうしないと、@Beforeメソッドあるいは@Afterメソッドが複数ある場合の相対的な順序は保証されません。
スイート全体に渡る初期化
JUnit 4では、JUnit 3には無い、新しい機能も導入されています。つまりクラス・スコープを持ったsetUp() メソッドとtearDown()メソッドです。@BeforeClass注釈が付いたメソッドはすべて、そのクラスの中にあるテスト・メソッドが実行される前に、必ず1度だけ実行されます。そして、@AfterClass注釈の付いたメソッドはすべて、そのクラスの中にあるテストがすべて実行された後、必ず1度だけ実行されます。
例えば、そのクラスの中にある各テストが、初期化や廃棄にコストがかかる、データベース接続や、ネットワーク接続、非常に大きなデータ構造、あるいはその他のリソースを使う場合を考えてみてください。それらをテストの度に作り直す代わりに、1度だけ作り、1度で廃棄することができるのです。この方法を利用すると、一部のテストケースはずっと早く実行するようになります。例えば、サードパーティーのライブラリーの中にコールを行うエラー処理コードをテストする場合、私はよく、テストが始まる前にSystem.errをリダイレクトし、想定されるエラー・メッセージで出力が汚くならないようにします。そしてテストの終了後に、それを回復するのです。これを下記に示します。
// This class tests a lot of error conditions, which
// Xalan annoyingly logs to System.err. This hides System.err
// before each test and restores it after each test.
private PrintStream systemErr;
@BeforeClass protected void redirectStderr() {
systemErr = System.err; // Hold on to the original value
System.setErr(new PrintStream(new ByteArrayOutputStream()));
}
@AfterClass protected void tearDown() {
// restore the original value
System.setErr(systemErr);
} |
テストの度に、テストの前後でこれを行う必要はありません。ただし、この機能には注意してください。この機能には、テストの独立性に違反し、予期しない結合を呼ぶ可能性があるのです。もし、あるテストが、@BeforeClassが初期化したオブジェクトを何らかの原因で変更すると、他のテストの結果に影響を与える可能性があります。テスト・スイートの中に順序に関する依存性が発生し、バグを隠してしまうかも知れません。どんな最適化でも同じですが、プロファイリングやベンチマークの結果、本当に問題があると分かってから、これを実装すべきです。とはいえ私は、あまりに実行に時間がかかるため、必要な回数だけ実行できないテスト・スイート(特に、数多くのネットワーク接続やデータベース接続が必要なもの)を1つならず見てきています。(例えば、LimeWareテスト・スイートは実行に2時間以上かかります。)こうしたテスト・スイートをスピードアップでき、プログラマーがもっと頻繁にテストを実行するように仕向けられるものであれば、どんなものでもバグの削減につながるはずです。
例外をテストする
例外のテストは、JUnit 4で行われた最大の改善の1つです。古いスタイルでの例外テストでは、例外を投げるコードの周囲にあるtryブロックをラップし、そのtryブロックの最後にfail()ステートメントを追加します。例えば、このメソッドは、ゼロで割るとArithmeticExceptionを投げることをテストします。
public void testDivisionByZero() {
try {
int n = 2 / 0;
fail("Divided by zero!");
}
catch (ArithmeticException success) {
assertNotNull(success.getMessage());
}
} |
このメソッドは醜いばかりではなく、(テストがパスするにせよ失敗するにせよ)一部のコードが実行されないため、コード・カバレージ・ツールが失敗しがちなのです。JUnit 4では、例外を投げるコードを書くことができ、また注釈を使って、例外が想定されていることを宣言できるようになっています。
@Test(expected=ArithmeticException.class)
public void divideByZero() {
int n = 2 / 0;
} |
もし例外が投げられないと(あるいは別の例外が投げられると)、テストはフェールします。ただし、例外に関する詳細メッセージや他のプロパティーが必要な場合には、相変わらず、古いtry-catchスタイルを使う必要があります。
無視されるテスト
皆さんの中には、実行に異常に時間がかかるテストを持っている人がいるかも知れません。そのテストにはもっと速く実行して欲しいものですが、そのテストが行っていることが、基本的に複雑、あるいは遅いのです。リモートのネットワーク・サーバーにアクセスするようなテストは多くの場合、このカテゴリーに入ります。もし皆さんが、そのテストを止めてしまう可能性のある何かに関して作業しているのでなければ、長時間実行するテスト・メソッドをスキップし、『コンパイル、テスト、デバッグ』のサイクルをスピードアップしたいと思うでしょう。また、皆さんが制御できる範囲外の理由で、テストがフェールしている場合もあるでしょう。例えば、W3CのXIncludeテスト・スイートは、まだJavaがサポートしていないUnicodeエンコーディングの幾つかに対する自動認識をテストします。こうした場合、赤いバー記号を無理に眺める代わりに、そうしたテストに@Ignore注釈を付けてしまえばよいのです。これを次に示します。
// Java doesn't yet support
// the UTF-32BE and UTF32LE encodings
@Ignore public void testUTF32BE()
throws ParsingException, IOException, XIncludeException {
File input = new File(
"data/xinclude/input/UTF32BE.xml"
);
Document doc = builder.build(input);
Document result = XIncluder.resolve(doc);
Document expectedResult = builder.build(
new File(outputDir, "UTF32BE.xml")
);
assertEquals(expectedResult, result);
} |
テスト・ランナーは、こうしたテストを実行せず、テストがスキップされたことを示します。例えば、テキスト・インターフェースを使うと、テストをパスしたことを示すピリオドや、テストにフェールしたことを示す「E」の代わりに、「I」(ignoreを表します)が出力されます。
$ java -classpath .:junit.jar org.junit.runner.JUnitCore
nu.xom.tests.XIncludeTest
JUnit version 4.0rc1
.....I..
Time: 1.149
OK (7 tests) |
ただし、よく注意してください。そもそもテストが書かれたのには、何らかの理由があるはずです。もし、そのテストを永遠に無視してしまうと、それらがテストするはずであったコードが動作不良になるかもしれず、しかもその動作不良が、検出されないかも知れません。テストを無視することは、一時的な間に合わせであり、問題に対する真のソリューションではありません。
時間指定のテスト
パフォーマンスのテストは、ユニット・テストの中で最も面倒な領域です。JUnit 4もこの問題を完全解決はしていませんが、前進が図られています。テストにはタイムアウト・パラメーターの注釈を付けることができます。そのテストの実行に、もし規定されたミリ秒以上かかる場合は、テストはフェールします。例えば次のテストは、その前にフィクスチャー(fixture)の中で設定された、文書中の全要素を見つけるのに0.5秒以上かかると、フェールします。
@Test(timeout=500) public void retrieveAllElementsInDocument() {
doc.query("//*");
} |
ネットワークのテストには、(単純なベンチマーキングの他に)時間指定のテストも有効です。例えば、あるテストが接続しようとしているリモート・ホストやデータベースがダウンしている場合、あるいは遅い場合、他のテストをホールド状態にしないように、そのテストをバイパスすることができます。また、良質なテスト・スイートは充分速く実行するものであり、プログラマーは大きな変更の後には毎回必ず、一日に何十回もそうしたテストを実行することができます。タイムアウトが設定できると、これが、より現実的になります。例えば次のテストは、http://www.ibiblio.org/xmlの構文解析に2秒以上かかると、タイムアウトします。
@Test(timeout=2000)
public void remoteBaseRelativeResolutionWithDirectory()
throws IOException, ParsingException {
builder.build("http://www.ibiblio.org/xml");
}
|
新しいアサーション(assertion)
JUnit 4では、配列の比較用に、2つのassert()メソッドが追加されています。
public static void assertEquals(Object[] expected, Object[] actual)
public static void assertEquals(String message, Object[] expected,
Object[] actual)
|
これらのメソッドは、配列の比較を最も明白な方法で行います。2つの配列が、同じ長さを持ち、それぞれの要素が、相手の配列で対応する要素と同じであれば、その2つの配列は同じです。それ以外の場合は、2つの配列は異なります。一方、あるいは両方の配列がヌルである場合も、同じように処理されます。
足りないもの
JUnit 4は根本的に新しいフレームワークであり、古いフレームワークのアップグレード版ではありません。JUnit 3に慣れた開発者は、JUnit 4では無くなっているものが幾つかあると思うかも知れません。
無くなっているものとして、最も明白なのは、GUIテスト・ランナーでしょう。テストがパスした時に出る、心を和ませる緑色のバーや、テストがフェールした時に出る、心配をかき立てる赤いバーを見たいのであれば、Eclipseのような、JUnitサポートを統合したIDEが必要です。JUnit 4では、Swingテスト・ランナーもAWTテスト・ランナーも更新されておらず、バンドルもされていません。
次に驚くことは、フェール(アサート・メソッドによってチェックされる、予期されるエラー)と、エラー(例外によって示される、予期せぬエラー)の間に区別が無くなったことです。JUnit 3テスト・ランナーは、相変わらずこれらのケースを区別できますが、JUnit 4ランナーは区別できません。
最後に、JUnit 4には、複数のテスト・クラスからテスト・スイートを構築するsuite()メソッドがありません。代わりに、可変長引数リストを使って、テストの数を規定せずにテスト・ランナーに渡せるようになっています。
GUIテスト・ランナーが無くなったことに対して、私はあまり快く思っていません。しかし、その他の変更は、JUnitをさらに単純化するために有効だと思います。こうした点の説明のために、現在どれほどの量のドキュメンテーションやFAQが書かれているかを考えてみてください。その後で、JUnit 4ではそれらを全く説明しなくて良いのだと考えれば、その素晴らしさが分かるでしょう。
JUnit 4をビルドし、実行する
現在、JUnit 4のバイナリー・リリースはありません。新しいバージョンで実験してみたい場合には、SourceForgeにあるCVSリポジトリーをチェックする必要があります。分岐は、「Version4」(参考文献)です。注意すべき点として、ドキュメンテーションの大部分は更新されておらず、古い、3.x流の方法を説明しています。JUnit 4では、注釈やジェネリックスなど、Java 5言語レベルの機能を頻繁に使用するため、JUnit 4のコンパイルにはJava 5が必要です。
コマンドラインからテストを実行するための構文は、JUnit 3から少し変わっており、今度はorg.junit.runner.JUnitCoreクラスを使うようになっています。
$ java -classpath .:junit.jar org.junit.runner.JUnitCore
TestA TestB TestC...
JUnit version 4.0rc1
Time: 0.003
OK (0 tests)
|
互換性
BeckとGammaは、非常な努力を行って、前方互換性と後方互換性の両方を維持しています。JUnit 4テスト・ランナーは、何も変更しなくてもJUnit 3テストを実行できます。JUnit 4テストでの場合と同じように、皆さんが実行したい各テストのクラス名として完全修飾されたものを、単純にテスト・ランナーに渡せば良いだけです。ランナーは、どのテスト・クラスがどのバージョンのJUnitに依存しているかを判断し、適切に呼び出しを行います。
後方互換性は少し面倒ですが、JUnit 3テスト・ランナーでもJUnit 4を実行することができます。Eclipseなど、統合JUnitサポートを備えたツールは更新を行わなくてもJUnit 4を処理できるため、これは重要です。JUnit 3環境でJUnit 4テストが実行できるようにするには、JUnit4TestAdapterの中にJUnit 4テストをラップします。次のようなメソッドをJUnit 4テスト・クラスに追加すれば充分なはずです。
public static junit.framework.Test suite() {
return new JUnit4TestAdapter(AssertionTest.class);
} |
ただしJavaに関しては、JUnit 4は全く後方互換性がありません。JUnit 4は、完全にJava 5の機能に依存しています。Java 1.4以前のバージョンでは、コンパイルも実行もできません。
これから先は
JUnit 4はまだ完成していません。ドキュメンテーションの大部分を含めて、幾つかの重要部分が欠けています。私としては、皆さんのテスト・スイートを注釈やJUnit 4に変換してしまうことは、まだお勧めしません。しかし、開発は急ピッチで進んでおり、JUnit 4は非常に大きなものを約束してくれそうです。Java 2プログラマーは、しばらくの間JUnit 3.8にとどまるでしょうが、Java 5に移行した人は、それに合わせて、自分たちのテスト・スイートをこの新しいフレームワークに適応させることを考え始めるでしょう。
参考文献 学ぶために
-
Pragmatic Unit Testing in Java with JUnit
(Andy HuntとDave Thomasの共著, Pragmatic Programmers, 2003年)は、ユニット・テストに関する素晴らしい入門書です。
-
JUnit Recipes: Practical Methods for Programmer Testing
(J. B. Rainsberger著, Manning, 2004年)はJUnitに関して最も広く引用され、また参照されている本の1つです。
- Cedric Beustによるフレームワークである、TestNGは、現在JUnit 4で使われている注釈ベースのテスト・スタイルの先駆けとなったものです。
- 「TestNGでJavaユニット・テストを楽々行う」(Filippo Diotalevi著, developerWorks, 2005年1月)は、TestNGを紹介しています。
- 「AntとJUnitを用いた漸進的開発」(Malcolm Davis著, developerWorks, 2000年11月)は、コード例を挙げながら、特にAntとJUnitを使って、ユニット・テストの利点を解説しています。
- 「エクストリーム・プログラミングの神秘を解く: テスト主導型プログラミング」(Roy Miller著, developerWorks, 2003年4月)は、プログラマーとしての生産性や質が、テスト主導型プログラミングによって革命的に前進できること、またテストを書くための機構に関して解説しています。
- 「Keeping critters out of your code」(David Carewらの共著, developerWorks, 2003年6月)を読んで、WebSphere Application Developerと組み合わせてJUnitを使う方法を学んでください。
- 「IBM Coberturaでテスト対象範囲を調べる」(Elliotte Rusty Harold著, developerWorks, 2005年5月)を読んで、この手軽なオープンソース・ツールを使って未テストのコードを識別し、バグを見つける方法を学んでください。
議論するために
著者について  | 
|  | Elliotte Rusty Haroldはニューオーリンズ出身であり、時たま、おいしいgumbo(オクラ入りのスープ)を食べに帰っています。ただし現在はニューヨークのブルックリン近郊のProspect Heightsに、妻のBethと猫のCharm(charmed quarkからとりました)とMarjorie(義理の母の名前からとりました)と一緒に住んでいます。彼はPolytechnic Universityのコンピューター・サイエンスの非常勤教授として、Java技術とオブジェクト指向プログラミングを教えています。彼のCafe au Lait Webサイトは、インターネット上で最も人気のある独立系Javaサイトの一つです。また、そこから派生したCafe con Lecheは、最も人気のあるXMLサイトの一つです。彼の最近の著作には『Java I/O, 2nd edition』があります。現在はXML処理用のXOM APIやJaxen XPathエンジン、Jesterテスト・カバレッジ・ツールなどに取り組んでいます。 |
記事の評価
|