JUnit4をもっとやってみよう

公開日 : 2013-11-09
最終更新日 : $Date: 2013-12-19 01:22:44 +0900 (Thu, 19 Dec 2013) $

要約

この文書は、JUnit4をやってみようの続きです。JUnit4 について動かしてみた結果をまとめています。 この文書は技術的に正確であることを意図して書いてはいますが、どこかで大嘘をついていたり、経年により陳腐化しているかもしれません。 もっと有効な方法があることを見逃しているかもしれません。
姉妹ページ、JUnit4をやってみようJUnit4をやってみよう(Rules編)もどうぞ。
サンプルソースはhttps://github.com/kazurof/tryjunit4 においてあります。

目次

  1. Parameterizedを試す
    1. 素朴な例
    2. コンストラクタの省略
    3. テスト名のカスタマイズ
    4. テストの階層化
  2. Categoriesを試す

Parameterizedを試す

テスト対象の1つのメソッドに多様なパラメータを与えてテストしたい場合があります。例えば境界条件付近での挙動をテストする時です。 Parameterizedは、このようなテストの作成をサポートします。

素朴な例

package tryjunit4.parameterized;

import static org.junit.Assert.assertEquals;
import java.util.Arrays;

import org.junit.Test;
import org.junit.runner.JUnitCore;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

@RunWith(Parameterized.class)
public class ParameterizedTest {
	public static void main(String[] args) {
		JUnitCore.main(ParameterizedTest.class.getName());
	}

	@Parameters
	public static Iterable<Object[]> data() {
		return Arrays.asList(new Object[][] { { 1, 1 }, { -2, 2 }, { 3, 3 }, { -4, 4 } });
	}

	private int fInput;

	private int fExpected;

	public ParameterizedTest(int input, int expected) {
		System.out.printf("I am constructor.  input ->  %d ,expected ->  %d %n", input, expected);
		fInput = input;
		fExpected = expected;
	}

	@Test
	public void testNantoka() {
		System.out.printf("I am testNantoka.  fInput ->  %d ,fExpected ->  %d %n%n", fInput, fExpected);
		assertEquals(fExpected, Math.abs(fInput));
	}

}

実行結果(CLASSPATHで、junit4.11.jarが設定されているとする。)

$ java tryjunit4.parameterized.ParameterizedTest
JUnit version 4.11
I am constructor.  input ->  1 ,expected ->  1 
.I am testNantoka.  fInput ->  1 ,fExpected ->  1 

I am constructor.  input ->  -2 ,expected ->  2 
.I am testNantoka.  fInput ->  -2 ,fExpected ->  2 

I am constructor.  input ->  3 ,expected ->  3 
.I am testNantoka.  fInput ->  3 ,fExpected ->  3 

I am constructor.  input ->  -4 ,expected ->  4 
.I am testNantoka.  fInput ->  -4 ,fExpected ->  4 


Time: 0.01

OK (4 tests)

$ 

通常では @Testアノテーションが付与されているtestNantoka()メソッドが1回だけ実行されます。しかしこのソースの場合 Parameterized.classが12行目で付与されているので異なる動作になります。JUnitはまず @Parametersアノテーションが付いているメソッド(19行目)からテストデータを取得します。 Iterable<Object[]>と言うのは見慣れないかも知れませんが、いわゆる2次元配列のような形になります。

JUnitはこのIterableから取得できるObject[] を1個のテストデータの塊とみなし、それぞれの値をコンストラクタに引き渡します。 ちなみにObject[] の長さとコンストラクタのパラメータの数が異なる場合は例外がスローされテスト失敗となります。 コンストラクタが終了した後、テストメソッドが実行されます。このメソッド内でフィールドの値を使ったテストが可能というわけです。

結果、テストメソッドの実行回数はIterableの長さと同じになります。テスト対象メソッドに多数のテストデータを適用してテストが実行できるわけです。

コンストラクタの省略

フィールドを初期化するだけのコンストラクタを書くのはめんどくさいという向きには、コンストラクタを書かないやり方もあります。 ついでに、テスト名の取得にTestNameを使ってみました。

package tryjunit4.parameterized;

import static org.junit.Assert.assertEquals;

import java.util.Arrays;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.runner.JUnitCore;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;

@RunWith(Parameterized.class)
public class NoConstructorTest {
	public static void main(String[] args) {
		JUnitCore.main(NoConstructorTest.class.getName());
	}

	@Rule
	public TestName name = new TestName();

	@Parameters
	public static Iterable<Object[]> data() {
		return Arrays.asList(new Object[][] { { 1, 1 }, { -2, 2 } });
	}

	@Parameter
	public int fInput;
	@Parameter(1) // need specify test data array index if it is not 0.
	public int fExpected;

	@Test
	public void testNantoka() {
		System.out.printf("MethodName --%s-- %n", name.getMethodName());
		assertEquals(fExpected, Math.abs(fInput));
	}
}


実行結果

$ java tryjunit4.parameterized.NoConstructorTest
JUnit version 4.11
.MethodName --testNantoka[0]-- 
.MethodName --testNantoka[1]-- 

Time: 0.01

OK (2 tests)

$ 

@Parameterアノテーションをpublicなフィールドに付与すると、 そのフィールドにテストデータが設定されます。アノテーションのパラメータにてテストデータの配列の添字を指定します。 デフォルトは0なので配列の先頭ならば省略できます。

TestNameによるテスト名取得ですが、 <テストメソッド名>[<テストデータ配列の添字>] となるようですね。

テスト名のカスタマイズ

テスト名はカスタマイズできます。@Parametersアノテーションのnameパラメータで指定できます。

package tryjunit4.parameterized;

import static org.junit.Assert.assertEquals;

import java.util.Arrays;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.runner.JUnitCore;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;

@RunWith(Parameterized.class)
public class SetTestNameTest {
	public static void main(String[] args) {
		JUnitCore.main(SetTestNameTest.class.getName());
	}

	@Rule
	public TestName name = new TestName();

	@Parameters(name = "添字{index}:  最初のパラメータ{0} 2個めのパラメータ {1}")
	public static Iterable<Object[]> data() {
		return Arrays.asList(new Object[][] { { 1, 1 }, { -2, 2 }, { 3, 3 } });
	}

	@Parameter
	public int fInput;
	@Parameter(1)
	// need specify test data array index if it is not 0.
	public int fExpected;

	@Test
	public void testNantoka() {
		System.out.printf("MethodName --%s-- %n", name.getMethodName());
		assertEquals(fExpected, Math.abs(fInput));
	}

	@Test
	public void testKantoka() {
		System.out.printf("MethodName --%s-- %n", name.getMethodName());
		assertEquals(fExpected, (int) Math.sqrt(fInput * fInput));
	}
}

実行結果

$ java tryjunit4.parameterized.SetTestNameTest
JUnit version 4.11
.MethodName --testKantoka[添字0:  最初のパラメータ1 2個めのパラメータ 1]-- 
.MethodName --testNantoka[添字0:  最初のパラメータ1 2個めのパラメータ 1]-- 
.MethodName --testKantoka[添字1:  最初のパラメータ-2 2個めのパラメータ 2]-- 
.MethodName --testNantoka[添字1:  最初のパラメータ-2 2個めのパラメータ 2]-- 
.MethodName --testKantoka[添字2:  最初のパラメータ3 2個めのパラメータ 3]-- 
.MethodName --testNantoka[添字2:  最初のパラメータ3 2個めのパラメータ 3]-- 

Time: 0.024

OK (6 tests)

$ 

25行目のnameパラメータでテスト名を指定できます。更に、{index} {0} {1} … という形でテストデータ配列の添字、パラメータ配列の0番目の値、 パラメータ配列の1番目の値… を指定することが出来ます。日本語も使えます。テストメソッドは英語のままでテストの名前に日本語が使えるのはなにげに画期的かもしれません。

実はこの機能にはちょっとしたバグがあります。テスト名に括弧 ( を入れて、EclipseにてRun as JUnit から実行させるとテスト名が 正しく取れません。参照 https://bugs.eclipse.org/bugs/show_bug.cgi?id=102512 2005年に報告されているので随分昔から残ってるんですね。

サンプルコードではテストメソッドを2本にしてみました。複数のメソッドを同じテストデータでテストすることは あまりないと思いますが、動作検証ということで。この場合テストデータが3件でテストメソッドが2本ということで、合計6回テストが実行されます。

テストの階層化

Parameterizedを使うと、テストメソッドとテストデータのすべての組み合わせがテスト実行されます。 コンソール実行ではわかりませんが、JUnit内部では階層構造をもたせているようです。それを可視化して見るために、 こんなコードを書いてEclipseで実行してみました。tryjunit4.suite.AllTests.classは、大量のテストケースを一括で動かす で使った例です。

package tryjunit4.parameterized;

import org.junit.runner.JUnitCore;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;

@RunWith(Suite.class)
@SuiteClasses({ ParameterizedTest.class, SetTestNameTest.class, 
	NoConstructorTest.class, tryjunit4.suite.AllTests.class })
public class AllTests {
	public static void main(String[] args) {
		JUnitCore.main(AllTests.class.getName());
	}
}

Eclipseによる実行結果

Parameterizedアノテーションが付いているテストはテストデータ配下にテストメソッドが連なる構造のようです。 ここにもカスタマイズしたテスト名が反映されています。検証していませんが、AntのJUnitReportにも反映されるのでは、、、と考えています。

市井には、テストメソッドを日本語で書く流儀があるようです。もしも@Testアノテーションにもテスト名が設定できたら、 日本人・外国人の混成チームでテスト実装は外国人、テスト名のメンテナンスは日本人というやり方が実用的になるかもしれません。 誰かが(私?)Pull Requestを投げてみても面白いでしょう。

Categoriesを試す。

大量のテストケースを一括で動かす。において、複数のテストケースを一括で実行するときのやり方を紹介しました。 このやりかたはテストケースを階層的にまとめていました。 Categories を使うと階層構造ではなくあるカテゴリーに属する・属さないという条件でテスト実行を制御できます。

やりかた

1.カテゴリーを表現するインターフェースを作成する。これは、識別できるのであれば文字列でもクラスでもよかったのかもしれません。 しかし、コンパイラにチェックを入れされることができること、バイトコードサイズが軽量になることからインターフェースを使うのがよいのだと思います。

package tryjunit4.categories;

public interface ColorCategory {

}

package tryjunit4.categories;

public interface FruitsCategory {

}

2.テストケースにorg.junit.experimental.categories.Categoryアノテーションを付与する。 アノテーションのパラメータに、カテゴリーを表現する型のクラスオブジェクトを設定してください。 Categoryアノテーションは、クラスにもメソッドにも付与できます。

package tryjunit4.categories;

import org.junit.Test;
import org.junit.experimental.categories.Category;


@Category(FruitsCategory.class)
public class NantokaTest {
	@Test
	public void testApple() {
		System.out.println("testApple");
	}

	@Category(ColorCategory.class)
	@Test
	public void testOrange() {
		System.out.println("testOrange");
	}
}

package tryjunit4.categories;

import org.junit.Test;
import org.junit.experimental.categories.Category;


public class KantokaTest {
	@Test
	public void testNantoka() {
		System.out.println("testNantoka");
	}

	@Category(FruitsCategory.class)
	@Test
	public void testGrape() {
		System.out.println("testGrape");
	}
}

3.org.junit.runner.RunWithアノテーションが付与されたテストケースを用意する。 CategoriesはSuiteクラスの子クラスです。 この例は、ExcludeCategoryを使っています。10行目のようなアノテーション付与で、 NantokaTestとKantokaTestからColorCategoryを除外したテストメソッドが実行されます。

package tryjunit4.categories;

import org.junit.experimental.categories.Categories;
import org.junit.experimental.categories.Categories.ExcludeCategory;
import org.junit.runner.JUnitCore;
import org.junit.runner.RunWith;
import org.junit.runners.Suite.SuiteClasses;

@RunWith(Categories.class)
@ExcludeCategory(ColorCategory.class)
@SuiteClasses({ NantokaTest.class, KantokaTest.class })
public class ExcludeTests {
	public static void main(String[] args) {
		JUnitCore.main(ExcludeTests.class.getName());
	}
}

実行結果(CLASSPATHで、junit4.11.jarが設定されているとする。)

$ java tryjunit4.categories.ExcludeTests
JUnit version 4.11
.testApple
.testGrape
.testNantoka

Time: 0.007

OK (3 tests)

$

ColorTestsが、付与されているtestOrangeは実行されませんでした。

別の例も試してみましょう。ExcludeCategoryと対になるIncludeCategoryを使ってみます。

package tryjunit4.categories;

import org.junit.experimental.categories.Categories;
import org.junit.experimental.categories.Categories.IncludeCategory;
import org.junit.runner.JUnitCore;
import org.junit.runner.RunWith;
import org.junit.runners.Suite.SuiteClasses;

@RunWith(Categories.class)
@IncludeCategory(ColorCategory.class)
@SuiteClasses({ NantokaTest.class, KantokaTest.class })
public class IncludeTests {
	public static void main(String[] args) {
		JUnitCore.main(IncludeTests.class.getName());
	}
}

実行結果(CLASSPATHで、junit4.11.jarが設定されているとする。)

$ java tryjunit4.categories.IncludeTests
JUnit version 4.11
.testOrange

Time: 0.005

OK (1 test)

$ 

IncludeCategoryの場合は、「@IncludeCategoryに書かれていないテストクラス、メソッドを除外する」という挙動になります。 つまり、書かれているテストクラスのみ実行します。

では、IncludeCategoryとExcludeCategory両方が設定されている場合はどうでしょうか?

package tryjunit4.categories;

import org.junit.experimental.categories.Categories;
import org.junit.experimental.categories.Categories.ExcludeCategory;
import org.junit.experimental.categories.Categories.IncludeCategory;
import org.junit.runner.JUnitCore;
import org.junit.runner.RunWith;
import org.junit.runners.Suite.SuiteClasses;


@RunWith(Categories.class)
@IncludeCategory(FruitsCategory.class)
@ExcludeCategory(ColorCategory.class)
@SuiteClasses({ NantokaTest.class, KantokaTest.class })
public class IncludeExcludeTests {
	public static void main(String[] args) {
		JUnitCore.main(IncludeExcludeTests.class.getName());
	}

}

実行結果(CLASSPATHで、junit4.11.jarが設定されているとする。)

$ java tryjunit4.categories.IncludeExcludeTests
JUnit version 4.11
.testApple
.testGrape

Time: 0.007

OK (2 tests)

$

この場合は、@Includeに書かれているものから@Excludeに書かれているものを除外して実行されます。 例では、@Includeに書かれている testApple(), testOrange(), testGrape() から、@Excludeに書かれているtestOrange()が除外されて実行されます。

では、IncludeCategoryとExcludeCategory両方設定されていない時はどうでしょうか? この場合は、@SuiteClassesに設定されているテストがすべて実行されます。

まとめてみました。

@Includeのみ
@Includeに書かれているテストのみ実行。
@Excludeのみ
@Excludeに書かれているテスト以外を実行。
@Includeと@Exclude両方
@Includeに書かれているテストから@Excludeに書かれているテストを除外して実行。
@Includeと@Excludeの記載なし
すべて実行する。

条件に含まれる・含まれないというやり方で実行するテストを選択する枠組みとしては、至極まっとうな印象を受けました。 これが必要になる状況はぱっとは思いつきませんが、ツール・フレームワークとしては当然あるべき機能でしょう。 パッケージ名からして実験的機能のようですが、そのような扱いでなくなることを期待したいです。


creative commons BY
この文書は 表示 2.1 日本 (CC BY 2.1) によってライセンスされます。

トップページに戻る