JUnit 4 & TestNG

最終更新日:2006/09/01
ogwa567@oki.com

目次

JUnit 4 と TestNG について

JUnit 4 は JUnit 3.8 を Java 5 のアノテーションに対応させて各種の制約(テストケースは TestCase を継承する必要があったり、テストメソッドは "test" という名前で始めなければならない)を取り除いたものになっています。若干機能拡張もなされていますが、アノテーション対応が目玉と言って良いでしょう。2006/09/01 現在の最新版は 4.1 です。

TestNG(NG は Next Generation の略)は アノテーションに本格対応したテスティングフレームワークで、JUnit に比べて機能が豊富であることが特徴です。2006/09/01 現在の最新版は 5.1です。

JUnit 4 は Eclipse 3.2 でサポートされているため、Eclipse 3.2 を使うならプラグインの導入や Jar のインストールといった設定無しでそのまま使えます。TestNG は Eclipse 向けのプラグインが提供されており、プラグインを導入したり Jar をインストールすることで Eclipse 上から使うことができます。

以下では Eclipse 3.2 を使うことを前提に、両テストツールについて紹介します。

JUnit 4

テスト対象クラス

以下は今回テスト対象とするクラス UserManager のソースコードです。後述する TestNG でもこのクラスをテスト対象とします。

package sample;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;

/**
 * ユーザの管理を行う。
 * テストツールの効果を確認するために敢えて問題のある実装をしています。 
 */
public class UserManager {

    private List<User> users = new ArrayList<User>();
    private int nextUserID = 0;
    private DecimalFormat formatter = new DecimalFormat("000000");

    /**
     * ユーザを新規登録する
     */
    public User registUser(String name, String mailAddr, String telNo) {
        nextUserID++;
        User user = new User(formatter.format(nextUserID), name, mailAddr, telNo);
        users.add(user);
        return user;
    }

    /**
     * 現在登録されているユーザ数を取得する。
     */
    public int getUserCount() {
        // 擬似ループ
        for (int i = 0; i < 100000000; i++) {}
        return users.size();
    }

    /**
     * 指定のユーザIDを持つユーザを削除する
     */
    public void deleteUser(String userID) throws UserNotFoundException {
        User target = null;
        for (User user : users) {
            if (user.getUserID().equals(userID)) {
                target = user;
                break;
            }
        }
        if (target != null) {
            users.remove(target);
        } else {
            throw new UserNotFoundException("UserID:" + userID + " を持つユーザは存在しません");
        }
    }
}

サンプルテストケース

上記のクラスに対する JUnit 4 のテストケースを以下に示します。

package sample;

import static org.junit.Assert.*;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class UserManagerTest {

    static UserManager manager;
    
    @BeforeClass
    public static void beforeClass() throws Exception {
        manager = new UserManager();
    }

    /**
     * 登録時に渡したユーザ情報を含むユーザオブジェクトが返されること
     */
    @Test
    public void checkRegisterdUserInfo() throws Exception {
        User user = manager.registUser("foo", "foo@bar.com", "123456789");
        assertEquals("foo", user.getName());
        assertEquals("foo@bar.com", user.getMailAddr());
        assertEquals("123456789", user.getTelNo());
    }
    
    /**
     * 登録時に同じIDが生成されないこと
     */
    @Ignore("現バージョンではテスト不要")
    @Test
    public void checkDuplicatoin() throws Exception {
        User user1 = manager.registUser("foo", "foo@bar.com", "123456789");
        User user2 = manager.registUser("foo", "foo@bar.com", "123456789");
        assertFalse(user1.getUserID().equals(user2.getUserID()));
    }
    
    /**
     * 存在しないユーザを削除しようとすると、UserNotFoundExceptionがスローされること
     */
    @Test(expected=UserNotFoundException.class)
    public void deleteInvalidUser() throws Exception {
        manager.deleteUser("hogehoge");
    }
    
    /**
     * ユーザ数取得が10ms以内に終わること
     */
    @Test(timeout=10)
    public void checkGetUserPerformance() throws Exception {
        manager.getUserCount();
    }
    
}

このテストケースの実行結果は以下のようになります。checkDeletePerformance の実行結果は環境(PCのスペック)に依存するため、成功する場合もあります。

準備

テストケースを作成/実行するプロジェクトへ JUnit 4 のライブラリを追加する必要があります。

プロジェクトを右クリック>プロパティ>Java のビルド・パス>「ライブラリー」タブ>ライブラリーの追加>「JUnit」 を選択し次へ>JUnit ライブラリー・バージョンで JUnit 4 を選択すれば、JUnit 4 のライブラリがプロジェクトへ追加され、ビルドや実行ができるようになります。

テストケース作成の注意点(JUnit 3.8 からの変更点)

JUnit 3.8 → JUnit 4 の最大の変更点はアノテーション対応ですが、ここではアノテーション以外の変更点を挙げます

org.junit.TestCase は継承不要
JUnit 3.8 では テストケースは org.junit.TestCase を必ず継承する必要がありましたが、JUnit 4 では継承する必要はありません。
org.junit.Assert の static import
TestCase を継承しない場合、Assert クラスの static メソッドをメソッド名だけで呼び出すことができないため、Assert.assertEquals() のように使わなければなりません。この煩わしさを避けるために、org.junit.Assert を static import します。上のテストケースの 3行目がそれです。

@Test

JUnit 3.8 では テストメソッドは必ず "test" という名前で始める必要があという制約がありましたが、JUnit 4 ではその代わりに @Test アノテーションをテストメソッドに付けます。JUnit はこのアノテーションがついたメソッドをテストメソッドとみなしてくれるため、テストメソッド名は自由に付ける事ができます。

上のサンプルでは 4つのテストメソッドを定義しています。

@Before, @After

JUnit 3.8 の setup() と tearDown() に相当するアノテーションです。このアノテーションが付けられたメソッドは、それぞれ各テストメソッドの実行前と実行後に呼び出されます。@Test と同様に、メソッド名の制約がなくなりました。上の例では setUp(), teatDown() というメソッド名にしていますが、他の名前にすることもできます。また必要なければ省略可能です。

@BeforeClass, @AfterClass

各テストクラスの実行前と実行後に一度だけ呼び出すことを示すアノテーションです。JUnit 3.8 にはこのような機能は無かったので、新規追加された機能です。これもメソッド名は自由です。こちらも必要なければ省略可能です。

@Ignore

テストメソッドを一時的に無効にするアノテーションです。オプションでコメントも与えることができます。

@Test(expected=例外クラス)

JUnit 3.8 ではあるメソッドを実行した結果、例外がスローされることをテストするためには以下のようなコードを書く必要がありました。

public void testException() {
    try {
        // テスト対象メソッド呼び出し
    } catch (Exception ex) {
        return;
    }
    fail();
}   

JUnit 4 ではどんな例外がスローされるのを期待するかを、@Test アノテーションの expected 属性を使って記述します。例えば上のコードは JUnit 4 では以下のようになります。

@Test(expected=Exception.class)
public void testException() {
    // テスト対象メソッド呼び出し
}   

Eclipse でテストケースを作成する場合、アノテーションの名前や属性名等を Ctrl + Space によって補完入力できます。例外のクラス名も Ctrl + Space で補完できるので積極的に使うと良いでしょう。

@Test(timeout=タイムアウト時間(ミリ秒))

@Test アノテーションの timeout 属性でテストのタイムアウト時間を宣言できます。ここで指定された時間内にテストが終了しなければ失敗とみなされます。ちなみにタイムアウトが発生した場合、JUnit 4 は Exception をスローするため、テスト結果は「エラー」(「失敗」ではない)となります。以下は checkDeletePerformance テストメソッドの実行結果の障害トレースです。

面白い機能だと思うのですが、いくつか注意する点があります。

・テストの準備時間も計測に含まれる

 テストメソッドは通常、テストの前提条件の構築⇒テストメソッド実行⇒検証 という流れになると思います。測定したいのはテストメソッドの実行時間だとしても、 timeout 属性で指定するタイムアウト時間にはテスト環境の作成と検証も含まれてしまいます。例えば以下のようにユーザの削除にかかる時間を評価したい場合、

/**
 * 1ユーザ削除が20ms以内に終了すること
 */
@Test(timeout=20)
public void checkDeleteUserTime() throws Exception {
    User user = manager.registUser("foo", "foo@bar.com", "123456789");
    manager.deleteUser(user.getUserID());
}

 削除を行うためにはまずユーザを登録する必要があるので1行目でユーザ登録しています。その後削除処理を実行していますが、タイムアウト時間はこの「ユーザ登録」と「ユーザ削除」を合わせた時間が評価されるため、純粋にユーザ削除にかかる時間だけを評価するのは難しいようです。ユーザ登録処理を @Before アノテーションを付けたメソッドに移しても同じです。@BeforeClass アノテーションに移せば大丈夫ですが、@BeforeClass を付けたメソッドはテストクラス実行前に一度しか実行されないため、テストメソッドの依存関係に注意する必要があります。

・Thread.sleep() で停止中にタイムアウトが発生しても、タイムアウト発生とみなされない

 仮に getUserCount メソッドが以下のように定義されているとします。

/**
 * 現在登録されているユーザ数を取得する。
 */
public int getUserCount() throws Exception {
    Thread.sleep(1000);
    return users.size();
}

 ここで以下のテストケースを実行すると、

/**
 * ユーザ数取得が10ms以内に終わること
 */
@Test(timeout=10)
public void checkGetUserPerformance() throws Exception {
    manager.getUserCount();
}

 このテストケースの実行結果は例外発生によるエラーです。コンソールには Thread.sleep() が割り込まれたことを示すスタックトレースが表示されます。timeout 属性で示す時間が経過すると、実行中のスレッドに割り込みがかかるようで、その結果として InterruptedException が発生したことがその理由です。

 次に getUserCount メソッドを以下のように変更すると、

public int getUserCount() throws Exception {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException ex) {}
    return users.size();
}

 こうすると、テストケースの実行結果は成功となります。同じように InterruptedException が発生しますが、例外を握りつぶしているので、「時間内に正しく終わった」とみなされるようです。と言っても実際のアプリケーションでは Thread.sleep() のような割り込みを許す処理はほとんどないと思います。今回 timeout 属性の動作確認のために Thread.sleep() を使いましたが、こういう結果になるということは覚えておいたほうが良さそうです。

実行方法

Eclipse からテストケースを実行するには、JUnit 3.8 の場合と同様にパッケージ・エクスプローラでテストケースを右クリックし、実行>JUnit テストを選べば OK です。

JUnit 4 のテストケースを JUnit 3.8 で実行する

JUnit 3.8 に対応したツールを利用する必要がある場合(例えば JUnit 3.8 対応カバレッジツールを使うような場合)、JUnit 4 のテストケースを JUnit 3.8 で動作させたいことがあります。そのためには、テストケースに以下のコードを追加し、

public static junit.framework.Test suite() {
    return new JUnit4TestAdapter(UserManagerTest.class);
}

 以下のようなコマンドで実行します( junit.jar は JUnit 3.8 の Jar ファイルです)。

java -cp classes;junit-4.1.jar;junit.jar junit.textui.TestRunner sample.UserManagerTest

 何故か junit.swingui.TestRunnder では実行に失敗するので textui の方で実行する必要があるようです。

JUnit 4 のテストケースを Eclipse 3.0/3.1 で実行する

JUnit の Web サイトから JUnit の媒体をダウンロードし、junit-4.1.jar をプロジェクトのビルド・パスに追加します。JUnit ビューが JUnit 4 に対応していないので、例えば @Ignore アノテーションを付けたテストメソッドが無視されたことが少しわかりにくいといった細かな違いはありますが、実行自体は問題なくできるようです。

TestNG

インストール

今回は Ecilpse のソフトウェア更新を使ってプラグインをインストールすることにします。尚、Eclipse 3.2 で動作確認していますが、Eclipse 3.0 以上ならば動作すると思います。

Eclipse のメニューからヘルプ>ソフトウェア更新>検索およびインストール>「インストールする新規フィーチャーを検索」を選び次へ>新規リモート・サイトを選び、以下のように入力します。

TestNG のプラグインが表示されるので、インストールします。

プロジェクトへの TestNG ライブラリの追加

プロジェクト内に作成する TestNG のテストケースをビルドして実行するには、プロジェクトのクラスパスに TestNG の ライブラリを追加する必要があります。プロジェクトを右クリック>プロパティ>Java のビルド・パス>「ライブラリー」タブ>外部 JAR の追加を選択し、Eclipse の plugin フォルダ下の org.testng.eclipse_5.1.0.0\lib\testng-jdk15.jar を追加して下さい(パスはプラグインのバージョンによって異なります)。

サンプルテストケース

package sample;

import static org.testng.Assert.*;
import org.testng.annotations.*;

public class UserManagerTest {

    static UserManager manager;

    @BeforeClass
    public static void beforeClass() throws Exception {
        manager = new UserManager();
    }

    /**
     * 登録時に渡したユーザ情報を含むユーザオブジェクトが返されること
     */
    @Parameters( { "user-name" })
    @Test
    public void checkRegisterdUserInfo(String userName) throws Exception {
        User user = manager.registUser(userName, "foo@bar.com", "123456789");
        assertEquals(userName, user.getName());
        assertEquals("foo@bar.com", user.getMailAddr());
        assertEquals("123456789", user.getTelNo());
    }

    /**
     * テストデータを生成
     */
    @DataProvider(name = "userData")
    public Object[][] createUserData() {
        return new Object[][] { new Object[] { "foo", "foo@bar.com", "123456789" },
                new Object[] { "hoge", "hoge@hoge.com", "987654321" } };
    }

    /**
     * 登録時に同じIDが生成されないこと
     */
    @Test(dataProvider = "userData")
    public void checkDuplicatoin(String userName, String mailAddr, String telNo)
            throws Exception {
        User user1 = manager.registUser(userName, mailAddr, telNo);
        User user2 = manager.registUser(userName, mailAddr, telNo);
        assertFalse(user1.getUserID().equals(user2.getUserID()));
    }

    /**
     * ユーザを削除できること
     */
    @Test(groups = { "delete" })
    public void deleteUser() throws Exception {
        User user = manager.registUser("foo", "foo@bar.com", "123456789");
        manager.deleteUser(user.getUserID());
    }

    /**
     * 存在しないユーザを削除しようとすると、UserNotFoundExceptionがスローされること
     */
    @Test(expectedExceptions = UserNotFoundException.class)
    public void deleteInvalidUser() throws Exception {
        manager.deleteUser("hogehoge");
    }

    /**
     * 同時接続ユーザ数が 5 の場合、それぞれ getUserCount
     * 呼び出しが100ms以内に終わること
     */
    @Test(timeOut = 1000, invocationCount = 10, threadPoolSize = 5)
    public void checkDeletePerformance() throws Exception {
        manager.getUserCount();
    }
}

アノテーション

TestNG の主なアノテーションを以下に示します。詳細は TestNG のドキュメントを参照して下さい。

アノテーション 説明
@BeforeSuite テスト・スイート内ののすべてのテストが実行される前に実行される
@AfterSuite テスト・スイート内ののすべてのテストが実行された後に実行される
@BeforeTest テスト実行前に実行される
@AfterTest テスト実行後に実行される
@BeforeClass テストクラス内のすべてのテストが実行される前に実行される。JUnit 4 の@BeforeClass と同じです。
@AfterClass テストクラス内のすべてのテストが実行された後に実行される。JUnit 4 の@BeforeClass と同じです。
@BeforeMethod テストメソッドが呼び出される前に実行される。JUnit 4 の@Before に相当します。
@AfterMethod テストメソッドが呼び出された後に実行される。JUnit 4 の@After に相当します。
@DataProvider テストメソッドへのデータを供給するメソッドを示す
@Factory オブジェクトのファクトリメソッドを示す
@Parameters テストメソッドへ渡すパラメータを示す
@Test テストメソッドを示す


@Test アノテーションには以下の属性を設定できます。

属性 説明
dataProvider データプロバイダの名前
expectedExceptions 期待する例外のリスト
groups グループ名
invocationCount メソッド実行回数
timeOut テストのタイムアウト時間(ミリ秒)
threadPoolSize スレッドプールサイズ。invocationCount 属性が設定されていなければこの属性は無視される。

testng.xml

TestNG は JUnit と同様に Eclipse 上でテストクラスを選択して実行できますが、それ以外に JUnit のテスト・スイートに相当する設定を XML で記述し、起動することができます。

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">

<suite name="Suite1" verbose="1">
  <parameter name="user-name" value="foo"/>
  <test name="StandardTest">
    <groups>
      <run>
        <exclue name="delete"/>
      </run>
    </groups>
    <classes>
      <class name="sample.UserManagerTest"/>
      <class name="..."/>
    </classes>
  </test>
  <test name="PerformanceTest">
    <packages>
      <package name="sample.performance"/>
      <package name="..."/>
    </packages>
  </test>
</suite>

上記のように実行するテストケースをクラス名やパッケージ名で指定します。後述のテストパラメータ等もここで設定することができます。

テストパラメータ

TestNG ではテストメソッドにパラメータを与えることができます。テストメソッドに @Parameters アノテーションを追加し、その引数にパラメータリスト(配列)を与えます。テストメソッドにも @Parameter で与えた引数と同じ数だけ引数を追加します。

以下では user-name というパラメータを一つ与えています。

@Parameters( { "user-name" })
@Test
public void checkRegisterdUserInfo(String userName) throws Exception {
    User user = manager.registUser(userName, "foo@bar.com", "123456789");
    ...
}

この user-name へ実際の値を与えるには、 testng.xml で <parameter> 要素を使います。前述の testng.xml では user-name パラメータの値を "foo" としています。

@Parameters の引数 "user-name" に渡される値は、 checkRegisterduserInfo() メソッドの引数 userName に渡されます。それぞれの名前を同じにする必要は無く、引数の位置によって値が渡されます。ですが、混乱を避けるためにも引数の名前は合わせた方が無難ではないでしょうか。

DataProvider

パラメータが複数あり、色々なパターンでテストしたい場合、XML にそのすべてのパラメータを記述するのは非常に面倒です。@DataProvider アノテーションを使うと、プログラム内でパラメータを生成して渡すことができます。

まず以下のように @DataProvider アノテーションを使ってテストデータを生成し、

@DataProvider(name = "userData")
public Object[][] createUserData() {
    return new Object[][] { new Object[] { "foo", "foo@bar.com", "123456789" },
                            new Object[] { "hoge", "hoge@hoge.com", "987654321" } };
}

テストメソッドの @Test アノテーションに dataProvider パラメータを追加し、どの dataProviderを参照するか指定します。

@Test(dataProvider = "userData")
public void checkDuplicatoin(String userName, String mailAddr, String telNo)
        throws Exception {

@DataProvider アノテーションをつけたメソッドは、必ず Object[][] または Iterator<Object>[] を返す必要があります。上の例では Object[2][3] の配列を返却しているので、checkDuplication メソッドは 2回実行されることになります。

グループ

テストメソッドをグループ分けし、どのテストグループを実行するか制御できます。以下のように @Test アノテーションの groups 属性で所属するグループを宣言し、

@Test(groups = { "delete" })

testng.xml で実行するグループ、もしくは実行しないグループを指定します。上の testng.xml では <exclude> によって実行しないグループを指定しています。実行するグループを指定する場合は <include> を使います。

尚、このグループ属性は @BeforeClass 等にも反映されます(groups 属性は @Test だけでなく @Before*, @After* アノテーションにも付けられます)。そのため、特に <include> を使って実行するグループを絞り込む場合は、関連する @Before*, @After* にも groups 属性が必要になるので注意が必要です。

マルチスレッド

@Test アノテーションの invocationCountthreadPoolSize 属性を使ってテストメソッドをマルチスレッドで動作させることができます。invocationCount は実行回数、threadPoolSize は同時実行スレッド数です。例えば以下のように記述した場合、

@Test(timeOut = 1000, invocationCount = 10, threadPoolSize = 5)

5つのスレッドによって合計10回(個々のスレッドが10回でなく、トータルで10回)実行されます。timeOut と組み合わせることによって性能テストやデッドロックの検出が可能です。

尚、UserManager.java の registUser メソッドでは敢えて userID を static に宣言し、スレッドアンセーフにしていました。そのため同時にユーザ登録が発生した場合、IDが重複する可能性があります。これを TestNG で検証できないか考えたのですが、結局やり方がわかりませんでした。

重複ID を検出するには、

  1. 複数のスレッドで同時にユーザ登録処理を行う
  2. その結果得られる User オブジェクトの userID に重複が無いか調べる

を行う必要があります。1は TestNG の invocationCount と threadPoolSize でできるのですが、2はすべてのスレッドが終了した後に一度だけ実行して欲しいものなので、テストメソッドの中に assetion を書くのは間違いです。

TestNG の機能を利用せず、自分でスレッドを起こせば可能ですが、テストはシンプルにすべきという考え方からは外れてしまいます。こういう用途ではなく、timeOut 属性と組み合わせて使う方法が正しい(というより想定された)使い方なのかもしれません。

JUnit 4 テストケースを TestNG テストケースへ変換

JUnit 4 のテストケースを Eclipse で開き、エディタ上で Ctrl + 1 とすると以下のような Quick Fix が表示されます。

Convert to TestNG (Annotations) を選択すると、TestNG のアノテーションに変換してくれます。ただ試してみたところ、TestNG 5.1 のプラグインでは TestNG 4 ベースのアノテーションに変換されてしまいました(TestNG 5.1 では deprecated 扱い)。今後プラグインのバージョンアップによって修正されるものと思います。


資料室へ戻る


Copyright (c) 2006 OKI Software Co., Ltd.