この文書は、JUnit4をやってみようの続きです。JUnit4
について動かしてみた結果をまとめています。
この文書は技術的に正確であることを意図して書いてはいますが、どこかで大嘘をついていたり、経年により陳腐化しているかもしれません。
もっと有効な方法があることを見逃しているかもしれません。
姉妹ページ、JUnit4をやってみよう、JUnit4をやってみよう(Rules編)もどうぞ。
サンプルソースはhttps://github.com/kazurof/tryjunit4 においてあります。
テスト対象の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 を使うと階層構造ではなくあるカテゴリーに属する・属さないという条件でテスト実行を制御できます。
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に設定されているテストがすべて実行されます。
まとめてみました。
条件に含まれる・含まれないというやり方で実行するテストを選択する枠組みとしては、至極まっとうな印象を受けました。 これが必要になる状況はぱっとは思いつきませんが、ツール・フレームワークとしては当然あるべき機能でしょう。 パッケージ名からして実験的機能のようですが、そのような扱いでなくなることを期待したいです。
この文書は
表示 2.1 日本 (CC BY 2.1)
によってライセンスされます。