テスティングフレームワーク JUnit

トップページに戻る

リファクタリング、XP開発の根幹をなすユニットテストを自動化するユニットテスト記述用フレームワークです。Kent Beckらによって作成されフリーで公開されています。

ユニットテストは、ソフトウェアをコンポーネント毎に独立してテストすることです。Javaでは、おおよそclassがユニットに相当します。

日本語で「単体テスト」と訳してしまうと、コーディング工程の直後に行う単体試験をイメージしてしまい、そこでパスすれば以降はもう使わないととられてしまいます。しかし、ユニットテストは、それ以降の工程でも使用します。以後の工程でバグが見つかった場合、まずそのバグを見つけるためのテストケースを追加します。(なぜなら、最初のテストコードではそのバグを検出できなかったのですから。) 次に、バグを修正した後このユニットテストを再度実行します。(これがリグレッションテスト:回帰テスト)。これをパスしてから再度コードを他のプログラムに結合して結合試験を行います。



JUnitの入手と設定

入手

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系を対象にしております。

http://www.junit.org/

ダウンロードしたファイルjunit3.8.1.zipは、zip形式でアーカイブされています。適切なディレクトリに解凍します。解凍用のツールがなくても、JDKのjarコマンドで解凍できます。

JUnit 3.8.1のアーカイブを展開したもの
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の実行環境に応じて使い分けることができます。

Window 2000 Cygwin 1.3上でのSwing版動作確認例
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のSwing TestRunnerの画面キャプチャ

JUnitのサンプル例では、3つのテストメソッドを実行し、うち2つにおいてテスト判定結果が否(Failure)、残り1つは実行中に何らかのエラー(この例ではjava.lang.ArithmeticException: / by zero)が発生したことを示しています。

普通自分でテストコードを書くとしても、テスト結果良否を自動で出すことはせず、せいぜい実行結果をprint文で出力するだけでした。それを見ながら結果が合っているか間違っているかをプログラマーが毎回頭で判断するので、面倒くさいし・・・。


ユニットテスト

作成

ユニットテストを実行するには、junit.framework.TestCaseを拡張(extends)したクラスを作ります。

Movieクラスのユニットテスト MovieTesterの記述

「リファクタリング」の第1章に出てきたリファクタリング対象となるクラスMovieのユニットテストを行うMovieTesterを作っていきます。

Movieクラスは、レンタルビデオの映画を表わすクラスです。属性に題名と映画種類(価格を決めるため)を持ち、操作としては題名の取得メソッドと、種類の設定メソッド、取得メソッドを持ちます。

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つを行います。

  1. junit.framework.TestCaseを継承する
  2. 引数にStringを取るコンストラクタを定義する
  3. テスト準備を行うsetUpメソッドを記述する
  4. 試験する項目ごとに、testで始まるメソッド名を付け、テスト内容を記述する
MovieTesterクラス
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

MovieTesterの実行

ユニットテストを実行するには、TestRunnerを使うと便利です。TestRunnerには、コマンドライン上で実行できるtextui版、JDK1.1 AWT上で実行できるui版、JDK1.2(Swing)上で実行できるswingui版があります。

  1. textui版
    java junit.textui.TestRunner refactoring.book1.MovieTester
  2. ui版
    java junit.ui.TestRunner refactoring.book1.MovieTester
  3. swingui版
    java junit.swingui.TestRunner refactoring.book1.MovieTester
TestRunnerを再起動せずにTestCaseをコンパイルし直して再実行する

JUnit3.2のTestRunnerは、TestCaseを書き直したりFixtureを書き直したりした場合、いったん終了させて再度実行しなければ変更が反映されません。頻繁にテストを記述(修正)して実行していると、これは少々面倒な作業です。そのようなときには、テストを実行する度にクラスを再ロードするTestRunnerを使うと便利です。textui版TestRunnerは、実行するたびにプログラムが終了するので、この再ロードのしくみは不要です。
なお、JUnit3.5では、TestRunnerに標準で再ロード機能が搭載されています。

  1. ui版
    java junit.ui.LoadingTestRunner refactoring.book1.MovieTester
  2. swingui版
    java junit.swingui.LoadingTestRunner refactoring.book1.MovieTester

1.〜3.の実行画面例

テスト記述

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)
注1
Object.equals()メソッドで判定します。
注2
==で判定します。
注3
JDK1.4からassertが予約語になったため、JUnit3.7からassertTrueに改名された。

頑健なコードになる理由

上述のようにテストを書いていると、ふと、「setPriceCodeでMovieクラスに定義してある定数以外の値を入れたらどうなるのだろうか」、と思いました。当初の設計(コーディング)では、setPriceCodeに定数以外の値を入れたことを想定していません。テストケースを書いていると、このように想定していなかった事態を思い付くことが多々あります。上述のMovieTesterではこの展開を記していませんが、この後、定数に定義されていない値を入れたら例外を投げるといった設計に進んでいく、「クラスによるタイプコードの置き換え」(リファクタリング)を実施するという展開が期待されます。


JUnit記述のTips

テストコードの分離

開発対象のプログラムとテスト用プログラムが混在するのは構成管理上望ましくない事態です。そこで、本来のコードとテスト用コードを分離するテクニックが必要となります。

1. package名を区別する

例えば開発プログラムのパッケージ名がfoo.barであるとき、foo.barパッケージ内のクラスをテストするコードはfoo.bar.testパッケージに配置します。

問題点としては、開発プログラムがパッケージローカルなアクセス制限をしている場合、テストコードからアクセスが困難になることが挙げられます。

2. package名は同一で、配置場所を分離

例えば開発プログラムのパッケージ名が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を加えます。

getter/setterメソッドがないprivate属性フィールドの内容をテストする

private宣言されたフィールドでgetメソッドが用意されていない場合、テストコード中からこのフィールドの内容が妥当かどうかテストすることが困難です。この時、Java 2 のReflection APIを使ってフィールドを取り出すことができます。java.lang.reflection.AccessibleObjectクラスのsetAccessible(boolean)メソッドを使います。

例)privateフィールドの値をテストするTestCase

private属性のフィールドname(型はString)を持つPersonクラスを考える。このクラスはsampleパッケージに属する。

privateフィールドを持つテスト対象クラス
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なフィールドへクラスの外部からアクセスするコーディングを行う。

Personクラスの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
1. Classオブジェクト取得
まずはReflectoinの基本であるClassオブジェクトを取得する。上述コード例のように、インスタンスにgetClassメソッドを呼び出す方法のほか、Class c = sample.Person.class; と記述する方法もある。
2. Fieldオブジェクト取得
今回のポイントはこのFieldオブジェクトである。Personクラスのフィールド名"name"であるFieldオブジェクトを取得する。このFieldオブジェクトは特定のインスタンスのフィールドを表して要るのではない点に注意。getFieldというメソッドは、publicなフィールドしか取得できない。
指定した名前のフィールドが存在しない場合、NoSuchFieldExceptionが発行される。
セキュリティ上アクセスが許されなければ、SecurityExceptionが発行される。
3. フィールドへのアクセス権を得る
2.で取得したフィールドはprivate属性なので、そのままではReflectionといえども別クラスなので値を取得できない。そこで、アクセス権を得るために、setAccessibleメソッドを呼んでいる。
4. フィールドの実際の値を取得
2.で取得し、3.でアクセス権を得たFieldオブジェクトに対してgetメソッドを呼び出し、defaultPersonインスタンスのnameフィールドの値を取り出す。戻り値がObject型であるため、キャストしている。

複数のテストケースをまとめて実行

JUnitを使ってテスティング&コーディングをしていくと、徐々にTestCaseが増えていきます。このTestCaseを1度にまとめて実行するには、suiteクラスメソッドを定義します。

MasterTestSuiteクラス
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メソッドを定義して一回だけ実施したい処理を記述します。

TestCase中一回だけ初期化する方法例
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から、見るべきもの

JUnit Primerに記載されていることから、見るべきものを紹介します。

Testing Idioms from JUnit Primer

イディオムとして、テスティングの秘訣を引用します。

一定時間以内に処理が終わらないと判定否とする

JUnit Load Testing Extensionsとしてソースが公開されているものの1つ、TimedTestクラス。TestDecoratorを継承し、一定時間以内にあるテストが終了しなかったらエラーとする。TestDecoratorを使ってこのような追加機能をフレームワークに持ち込む方法の参考としてもよい。

JUnit best practiceから、見るべきもの

JUnit best practiceに記載されているプラクティスを列挙します。


JUnitに関するリンク