リファクタリング、XP開発の根幹をなすユニットテストを自動化するユニットテスト記述用フレームワークです。Kent Beckらによって作成されフリーで公開されています。
ユニットテストは、ソフトウェアをコンポーネント毎に独立してテストすることです。Javaでは、おおよそclassがユニットに相当します。
日本語で「単体テスト」と訳してしまうと、コーディング工程の直後に行う単体試験をイメージしてしまい、そこでパスすれば以降はもう使わないととられてしまいます。しかし、ユニットテストは、それ以降の工程でも使用します。以後の工程でバグが見つかった場合、まずそのバグを見つけるためのテストケースを追加します。(なぜなら、最初のテストコードではそのバグを検出できなかったのですから。) 次に、バグを修正した後このユニットテストを再度実行します。(これがリグレッションテスト:回帰テスト)。これをパスしてから再度コードを他のプログラムに結合して結合試験を行います。
JUnit.orgサイトからJava用のテスティングフレームワークをダウンロードします。JUnitには、Java
2 SE 5.0以降のJavaにのみ対応しているVer.4.x系と、Java
2 SE 1.4.2以前も対応しているVer.3.x系と二つのバージョン体系が存在しています。2006.5.28時点ではそれぞれVer.4.1とVer.3.8.1が最新版です。
なお、本記事では、Ver.3.x系を対象にしております。
ダウンロードしたファイルjunit3.8.1.zipは、zip形式でアーカイブされています。適切なディレクトリに解凍します。解凍用のツールがなくても、JDKのjarコマンドで解凍できます。
| README.html | 概要、バージョン毎の新機能、解説文書へのリンク |
| cpl-v10.html | ライセンス文書。Common Public License v1.0 |
| doc/ | 解説文書:JUnit Cookbook, A cooks tour, TestInfected, FAQ |
| javadoc/ | JavaDoc形式のAPI Document |
| junit/ | サンプル、JUnit自体のテストケース(?) |
| junit.jar | JUnitテスティングフレームワークのクラスライブラリ |
| src.jar | JUnitテスティングフレームワークのソースコード |
JUnitテスティングフレームワークのクラスライブラリ(junit.jar)をクラスサーチパスに設定します。下記のどちらかの方法で設定します。(他にあるかな・・・)
JUnit 3.8.1を展開したディレクトリをカレントディレクトリにし、CLASSPATHにカレントディレクトリが含まれているのを確認後、TestRunner(テスト実行用UI)を起動します。
なお、TestRunnerには、Swing GUI版、AWT GUI版、そしてキャラクタUI版(コマンドライン版)の3種類あります。Javaの実行環境に応じて使い分けることができます。
junit3.8.1$ ls README.html cpl-v10.html doc/ javadoc/ junit/ junit.jar src.jar junit3.8.1$ echo $CLASSPATH .;d:\java\junit3.2\junit.jar junit3.8.1$ java junit.swingui.TestRunner junit.samples.SimpleTest |
Swing版テスト実行画面例

JUnitのサンプル例では、3つのテストメソッドを実行し、うち2つにおいてテスト判定結果が否(Failure)、残り1つは実行中に何らかのエラー(この例ではjava.lang.ArithmeticException: / by zero)が発生したことを示しています。
普通自分でテストコードを書くとしても、テスト結果良否を自動で出すことはせず、せいぜい実行結果をprint文で出力するだけでした。それを見ながら結果が合っているか間違っているかをプログラマーが毎回頭で判断するので、面倒くさいし・・・。
ユニットテストを実行するには、junit.framework.TestCaseを拡張(extends)したクラスを作ります。
「リファクタリング」の第1章に出てきたリファクタリング対象となるクラスMovieのユニットテストを行うMovieTesterを作っていきます。
Movieクラスは、レンタルビデオの映画を表わすクラスです。属性に題名と映画種類(価格を決めるため)を持ち、操作としては題名の取得メソッドと、種類の設定メソッド、取得メソッドを持ちます。
package refactoring.book1;
public class Movie {
public static final int CHILDRENS = 2;
public static final int REGULAR = 0;
public static final int NEW_RELEASE = 1;
private String title_;
private int priceCode_;
public Movie(String title, int priceCode) {
title_ = title;
priceCode_ = priceCode;
}
public int getPriceCode() {
return priceCode_;
}
public void setPriceCode(int arg) {
priceCode_ = arg;
}
public String getTitle() {
return title_;
}
} // Movie
|
MovieTesterクラスは、Movieクラスのユニットテスト(日本語にすると単体テスト)を行うテストケースを記述します。JUnitテスティングフレームワークでは、それぞれのユニットテストのことをテストケースと呼んでいます。一番簡単なテストケースの書き方は、次の4つを行います。
package refactoring.book1;
import junit.framework.TestCase;
// 1. junit.framework.TestCaseを継承する
public class MovieTester extends TestCase {
private Movie regularMovie_;
private Movie newReleaseMovie_;
private Movie childrensMovie_;
// 2. 引数にStringを取るコンストラクタを定義する
public MovieTester(String name) {
super(name);
}
// 3. テスト準備を行うsetUpメソッドを記述する
protected void setUp() throws Exception {
regularMovie_ = new Movie("Gone with wind", Movie.REGULAR);
newReleaseMovie_ = new Movie("Gradiator", Movie.NEW_RELEASE);
childrensMovie_ = new Movie("Tarzan", Movie.CHILDRENS);
}
/**
* 初期化した3つのMovieオブジェクトのgetPriceCodeメソッドが、正しく
* 初期化時の値を取得できたかテストする。
*/
public void testGetPriceCode() {
int regularCode = regularMovie_.getPriceCode();
assertEquals("REGULAR PriceCode.", Movie.REGULAR, regularCode);
int newReleaseCode = newReleaseMovie_.getPriceCode();
assertEquals("NEW_RELEASE PriceCode.", Movie.NEW_RELEASE,
newReleaseCode);
int childrensCode = childrensMovie_.getPriceCode();
assertEquals("CHILDRENS PriceCode.", Movie.CHILDRENS, childrensCode);
}
/**
* 初期化した3つのMovieオブジェクトのgetTitleメソッドが、正しく
* 初期化時の値を取得できたかテストする。
*/
public void testGetTitle() {
String regularTitle = regularMovie_.getTitle();
assertEquals("REGULAR Title.", "Gone with wind", regularTitle);
String newReleaseTitle = newReleaseMovie_.getTitle();
assertEquals("NEW_RELEASE Title.", "Gradiator", newReleaseTitle);
String childrensTitle = childrensMovie_.getTitle();
assertEquals("CHILDRENS Title.", "Tarzan", childrensTitle);
}
/**
* 新作ovieオブジェクトのsetPriceCodeメソッドを呼んで、NEW_RELEASEから
* REGULARに変更する。変更に使用した値と変更後Movieオブジェクトから
* getTitleメソッドを呼んで取得した値が等しいかをテスト。
*/
public void testSetPriceCode() {
newReleaseMovie_.setPriceCode(Movie.REGULAR);
int priceCode = newReleaseMovie_.getPriceCode();
assertEquals("set REGULAR Title.", Movie.REGULAR, priceCode);
}
} // MovieTester
|
ユニットテストを実行するには、TestRunnerを使うと便利です。TestRunnerには、コマンドライン上で実行できるtextui版、JDK1.1 AWT上で実行できるui版、JDK1.2(Swing)上で実行できるswingui版があります。
JUnit3.2のTestRunnerは、TestCaseを書き直したりFixtureを書き直したりした場合、いったん終了させて再度実行しなければ変更が反映されません。頻繁にテストを記述(修正)して実行していると、これは少々面倒な作業です。そのようなときには、テストを実行する度にクラスを再ロードするTestRunnerを使うと便利です。textui版TestRunnerは、実行するたびにプログラムが終了するので、この再ロードのしくみは不要です。
なお、JUnit3.5では、TestRunnerに標準で再ロード機能が搭載されています。
TestRunnerは、各テストメソッド(testXXX)をそれぞれ別のインスタンスで実行します。ですから、あるtestメソッドでインスタンス変数を書き換えても、別のtestメソッドには影響を及ぼしません。破壊テストをしても大丈夫ってとこでしょうか。
テストを実施するときに、あらかじめテスト対象のオブジェクトを用意しておきますが、その処理を各testに書くのは冗長なので、setUpメソッドに書きます。
テスト行為は、TestCaseが持つメソッド(実際は、TestCaseのスーパークラスAssert)を使います。上記では、AssertEqualsメソッドを呼び出しています。2つのオブジェクトの内容が等しい(equals)か否か(いわゆる良否)を判定し、等しくない場合には例外をthrowします。この例外はテストプログラムでキャッチする必要はなく、TestRunner側でキャッチし、テスト項目が成功か失敗かをカウントします。
public static void assertEquals(java.lang.String message,
java.lang.Object expected,
java.lang.Object actual)
message:このassert(表明)の詳細内容。
expected:テスト結果はこのオブジェクトに等しくなくてはならない判定基準
actual:テストを実行した結果得られたデータ(測定データ)。
上記MovieTesterクラスのtestGetTileメソッドでは、テスト対象となるMovieクラスのgetTitleメソッドのテストを行います。MovieTesterのsetUpメソッドで初期化されているregularMovieオブジェクトに対してgetTitleメソッドを呼び出し、得られた結果が本来もっているべきである値に等しいかをassertEqualsで判定しています。最初の判定では、regularMovie_オブジェクトに対してgetTitleメソッドを呼んで映画の題名文字列を取り出しています。regularMovie_オブジェクトはsetUpメソッド中でインスタンス生成しており、その時に題名として"Gone with wind"を入れています。そこで、このテスト判定ではこの"Gone with wind"とregularMovie_.getTitle()とを比較判定します。
assertEqualsのような判定メソッドは、assertで始まる14種類のメソッドが用意されています。また、assertXXメソッドでは判定できないような複雑なケースのために、TestCaseを記述する側が良否を判定し、否の場合にそれをTestRunnerに通知するためのfailメソッドが2種類あります。
| 判定する方法 | 使用するメソッド | メッセージをつけて使用するメソッド |
|---|---|---|
| 真偽で判定注3 | assert(boolean) | assert(String, boolean) |
| 2つの整数が等しいか判定 | assertEquals(long, long) | assertEquals(String, long, long) |
| 2つの浮動小数が等しいか判定 | assertEquals(double, double) | assertEquals(String, double, double) |
| 2つのオブジェクトが等しい注1か判定 | assertEquals(Object, Object) | assertEquals(String, Object, Object) |
| 2つのオブジェクトが同一注2か判定 | assertSame(Object, Object) | assertSame(String, Object, Object) |
| オブジェクトがnullでないことを判定 | assertNotNull(Object) | assertNotNull(String, Object) |
| オブジェクトがnullであることを判定 | assertNull(Object) | assertNull(String, Object) |
| テストの判定結果を否にする | fail() | fail(String) |
上述のようにテストを書いていると、ふと、「setPriceCodeでMovieクラスに定義してある定数以外の値を入れたらどうなるのだろうか」、と思いました。当初の設計(コーディング)では、setPriceCodeに定数以外の値を入れたことを想定していません。テストケースを書いていると、このように想定していなかった事態を思い付くことが多々あります。上述のMovieTesterではこの展開を記していませんが、この後、定数に定義されていない値を入れたら例外を投げるといった設計に進んでいく、「クラスによるタイプコードの置き換え」(リファクタリング)を実施するという展開が期待されます。
開発対象のプログラムとテスト用プログラムが混在するのは構成管理上望ましくない事態です。そこで、本来のコードとテスト用コードを分離するテクニックが必要となります。
例えば開発プログラムのパッケージ名がfoo.barであるとき、foo.barパッケージ内のクラスをテストするコードはfoo.bar.testパッケージに配置します。
問題点としては、開発プログラムがパッケージローカルなアクセス制限をしている場合、テストコードからアクセスが困難になることが挙げられます。
例えば開発プログラムのパッケージ名がfoo.barであるとき、このパッケージをテストするコードもfoo.barパッケージ指定します。ただし、同じ場所に両者が混在しないようにディレクトリを分けます。開発プログラムを/home/hoge/project-x/src/foo/barにおいたとして、テストコードは/home/hoge/project-x/test/foo/barに置き、CLASSPATHに/home/hoge/projext-x/src, /home/hoge/projext-x/testを加えます。
private宣言されたフィールドでgetメソッドが用意されていない場合、テストコード中からこのフィールドの内容が妥当かどうかテストすることが困難です。この時、Java 2 のReflection APIを使ってフィールドを取り出すことができます。java.lang.reflection.AccessibleObjectクラスのsetAccessible(boolean)メソッドを使います。
private属性のフィールドname(型はString)を持つPersonクラスを考える。このクラスはsampleパッケージに属する。
package sample;
public class Person {
private String name = "Momotaro";
public Person() {
}
public Person(String aName) {
name = aName;
}
}// Person
|
ここで、PersonクラスのTestCaseであるPersonTestクラスを作成する。普通にコーディングすると、Personクラスのnameフィールドはprivateであるため、PersonTestクラスのメソッドからはnameフィールドを参照できない。これではassert系のテストメソッドでテストを行うことができない。しかし、だからといってテストのためにPersonクラスのnameをデフォルトアクセス(パッケージローカル)やprotectedアクセスに緩めるのはよい考えといえるかどうか。。。
そこで、Java Reflection APIを使ってprivateなフィールドへクラスの外部からアクセスするコーディングを行う。
package sample;
import junit.framework.TestCase;
import java.lang.reflect.Field;
public class PersonTest extends TestCase {
private Person defaultPerson;
public PersonTest(String name) {
super(name);
}
protected void setUp() throws Exception {
defaultPerson = new Person();
}
public void testDefaultPerson() throws Exception {
Class c = defaultPerson.getClass(); // 1
Field nameField = c.getDeclaredField("name"); // 2
nameField.setAccessible(true); // 3
assertEquals("Default Person Name", "Momotaro",
(String)nameField.get(defaultPerson)); // 4
}
}// TestPerson
|
JUnitを使ってテスティング&コーディングをしていくと、徐々にTestCaseが増えていきます。このTestCaseを1度にまとめて実行するには、suiteクラスメソッドを定義します。
package refactoring.book1;
import junit.framework.TestCase;
import junit.framework.TestSuite;
public class MasterTestSuite extends TestCase {
public MasterTestSuite(String name) {
super(name);
}
public static Test suite() {
TestSuite suite = new TestSuite();
suite.addTest(new TestSuite(MovieTester.class));
suite.addTest(new TestSuite(RentalTester.class));
suite.addTest(new TestSuite(CustomerTester.class));
}
}
|
テストランナーでこのクラスを実行すると、suiteクラスメソッド中で指定した各テストケースを順次実行します。
TestCaseの初期化メソッドsetUp()は、テストメソッドの数だけインスタンスが生成されるので、その度に実行されます。プロセスで一回だけ初期化したいときには、junit.extensions.TestSetupクラスを使います。例えば、テストフィクスチャにRMIオブジェクトを作り、それへアクセスするテストを行うテストケースを考えます。RMIオブジェクトはすべてのテストに共通で1回だけ生成しbindしたいとき、TestCase#setUpに記述することはできません。このときは、TestCase拡張クラスにsuiteクラスメソッドを記述(オーバーライド)し、その中でTestSuiteインスタンスを作ってからTestSetupインスタンスで包んでしまい(デコレーターパターン)、そのTestSetupクラスのsetUpメソッドを定義して一回だけ実施したい処理を記述します。
import junit.framework.TestCase;
import junit.extensions.TestSetup;
public class RMITestServerTest extends TestCase {
static RMITestServer;
:
// 通常のsetUpやtestXXXの記述
:
static void oneTimeSetup() throws Exception {
System.setSecurityManager(new RMISecurityManager());
testServer = new RMITestServer();
Naming.rebind("TestServer", testServer);
}
public static Test suite() throws Exception {
TestSuite suite = new TestSuite(RMITestServerTest.class);
TestSetup wrapper = new TestSetup(suite) {
public void setUp() throws Exception {
oneTimeSetup();
}
};
return wrapper;
}
}
|
例外処理をはしょって放り投げてますが、JUnitではTestRunnerが拾ってくれます。
RMIでは、セキュリティマネージャの設定とリモートオブジェクトの生成とネーミングサービス(rmiregistry)への登録は、実行するプロセスで最初の1回だけです。そのときはTestSetupを使うのがスマートです。staticブロックや遅延初期化等のテクニックを駆使すればTestSetupがなくても出来るかもしれませんが、力を注ぐべきはテストコードではなく本来のアプリの方であることを忘れずに「テストは最低限の努力で最大の効果を出す」ことを目指しましょう。
JUnit Primerに記載されていることから、見るべきものを紹介します。
イディオムとして、テスティングの秘訣を引用します。
- 少しコーディングしたら少しテスト、・・・
- 出来るだけ頻繁にテストを実行する、少なくてもコンパイラを走らせる回数以上は。
- 1日(一晩)に一回はシステム中のテストを全部実行する。
- もっともやばそうだと思っているコード付近のテストを書くことから始めよう。
- テストへの投資の収益がもっとも最大になるようテストを書く。
- システムに機能を追加するときは、最初にテストを書く。
- デバッグをしていてSystem.out.println()を使っているようなら、代わりにテストケースを書く。
- バグが報告されたら、そのバグが見つかるようなテストケースを書く。
- 次に誰かにデバッグに協力を頼まれたなら、テストを書いて協力しよう。
- テスト全てにパスしていないソフトウェアを配ってはいけない。
JUnit Load Testing Extensionsとしてソースが公開されているものの1つ、TimedTestクラス。TestDecoratorを継承し、一定時間以内にあるテストが終了しなかったらエラーとする。TestDecoratorを使ってこのような追加機能をフレームワークに持ち込む方法の参考としてもよい。
JUnit best practiceに記載されているプラクティスを列挙します。