JUnit4のRunner概説

Runner が何かについては省略。テストの実行のしかたを決めるものと考えればよい。

Runner の指定方法

テストを実行するための Runner を指定するには、基本的にテストクラスに @RunWithアノテーションを設定し、そのパラメータに使いたい Runner のクラスを指定する。

例えば、以下のようにすると Parameterized が使われる。

import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

@RunWith(Parameterized.class)
public class MyTestClass {
...

@Ignore

@RunWith 以外で Runner を指定できるケースとして @Ignoreアノテーションがある。

これを指定すると IgnoredClassRunner という Runner によりクラス全体がテスト対象から除外される。

import org.junit.Ignore;

@Ignore
public class MyTestClass {
    @Test
    public void このテストは無視されます() {}
}

標準の Runner

テストクラスに @Ignore も @RunWith も指定しないと*1標準の Runner である BlockJUnit4ClassRunner が使用される。

@RunWith(BlockJUnit4ClassRunner.class) 

↑でも同じだが、このように明示的に指定する場合は JUnit4 クラスを使うことが推奨されている*2

@RunWith(JUnit4.class)

その他の Runner

@RunWith に指定できる JUnitに組み込みの Runner には以下のものがある。

Suite

同時に設定する @SuiteClasses に指定されたクラス全てに対して再帰的にテストを実施する。

例えば、以下のように設定したクラス MyTestSuite に対してテストを実行すると、MyTest1, MyTest2 それぞれに対してテストが実行される。それぞれに対するテストは直接実行した場合と同じように行われるので、@Ignore を指定していれば無視されるし、@RunWith(Suite.class) を指定すれば更に再帰的に実施される。

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

@RunWith(Suite.class)
@SuiteClasses(MyTest1.class, MyTest2.class)
public class MyTestSuite {
    // 中身は別に要らない
}

この Suite と以下に出てくる Categories, Enclosed は「他のクラスをテスト対象として指定するもの」で、自クラスはテスト対象にならない。なので自クラス内に @Test、@Before、@After アノテーションが付いたメソッドがあっても実行されない(@BeforeClass, @AfterClass は実行される)。

Categories

Suite のサブクラス。@SuiteClasses に指定されたクラス全てに対して再帰的にテストを実施するが、その際に @Category アノテーションによるフィルダリングを行う。

用例は CategoriesJavadoc にある。

なお、@Category に指定するクラスは単なるマーカーでありクラスの内容は全く無関係であるため、以下のように既存の適当なクラスを指定しても問題なかったりする。

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

@RunWith(Categories.class)
@SuiteClasses({MyTestSuite.Test1.class, MyTestSuite.Test2.class})
// @Category(Integer.class) のテストのみ実行
@IncludeCategory(Integer.class)
public class MyTestSuite {

    @Category(Integer.class)
    public static class Test1 {
        @Test public void test1() {}
    }

    @Category(Long.class)
    public static class Test2 {
        @Test public void test2() {}
    }
}
Enclosed

これも Suite のサブクラスだが、テスト対象のクラスを @SuiteClass で指定する必要が無く、自動的にインナークラス(Class#getClasses() で取得できるクラス)全てをテスト対象としてくれるという点のみ異なる。

例えば、以下のようにするとクラス Test1, Test2 が両方ともテスト対象となる。

import org.junit.experimental.runners.Enclosed;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(Enclosed.class)
public class MyTestSuite {

    public static class Test1 {
        @Test public void test1() {}
    }

    public static class Test2 {
        @Test public void test2() {}
    }
}

このときの実行のされ方は Suite と同じなので、@Before や @After 等も Test1, Test2 のそれぞれに指定したものが使用され、MyTestSuite に指定しても実行されない。

ただ、@BeforeClass, @AfterClass は実行されるので、共通の初期化を行うことは一応可能だ。

Parameterized

ParameterizedクラスのJavadocにあるように、パラメータリストを返すメソッドを作成し@Parameters アノテーションをつけると、そのパラメータ全てに対して @Test をつけたテストが繰り返し実行される。

これも Suite のサブクラスだが、上の3つとは異なりテスト対象はあくまでも自クラスになるため @Before や @After も各テストの実行前後に実行される。

Theories

大量の条件があって、それら全ての組み合わせに対してテストが通ることを確認したい場合に使える? Runner。

例えば、日本人の伝統的な名前であれば true を返す NameChecker.isJapaneseClassicName(String) というメソッドがあり、これが色々な名字・名前の組み合わせについて true と判定してくれるかどうかテストしたいとする。その場合、以下のようなテストになるかもしれない。

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

import java.util.Arrays;
import java.util.List;

import org.junit.experimental.theories.ParameterSignature;
import org.junit.experimental.theories.ParameterSupplier;
import org.junit.experimental.theories.ParametersSuppliedBy;
import org.junit.experimental.theories.PotentialAssignment;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;

@RunWith(Theories.class)
public class TheoryTest {

    // テストパラメータ(名字)の一覧を返すクラス
    public static class LastNameSupplier extends ParameterSupplier {
        @Override
        public List<PotentialAssignment> getValueSources(ParameterSignature sig) {
            return Arrays.asList(new PotentialAssignment[]{
                    PotentialAssignment.forValue("名字1", "山田"),
                    PotentialAssignment.forValue("名字2", "田中"),
                    PotentialAssignment.forValue("名字3", "佐藤"),
                    PotentialAssignment.forValue("名字4", "鈴木"),
            });
        }
    };

    // テストパラメータ(名前)の一覧を返すクラス
    public static class FirstNameSupplier extends ParameterSupplier {
        @Override
        public List<PotentialAssignment> getValueSources(ParameterSignature sig) {
            return Arrays.asList(new PotentialAssignment[]{
                    PotentialAssignment.forValue("名前1", "太郎"),
                    PotentialAssignment.forValue("名前2", "花子"),
            });
        }
    };

    @Theory
    public void テスト(
            @ParametersSuppliedBy(FirstNameSupplier.class) String firstName,
            @ParametersSuppliedBy(LastNameSupplier.class) String lastName) throws Exception {
        System.out.println("checking name: " + lastName + firstName);
        assertThat(
            NameChecker.isJapaneseClassicName(lastName + firstName),
            is(true));
    }
}

必要な作業は、

  • テストしたいパラメータ値を PotentialAssignment として返す ParameterSupplier のサブクラスを作成する
  • テストメソッドには @Test の代わりに @Theory を付ける
  • テストメソッドのパラメータに @ParametersSuppliedBy(作成したParameterSupplierのサブクラス) を指定する

となる。

これを実行すると、以下のように全てのパラメータの組み合わせについてテストが実施される。

checking name: 山田太郎
checking name: 田中太郎
checking name: 佐藤太郎
checking name: 鈴木太郎
checking name: 山田花子
checking name: 田中花子
checking name: 佐藤花子
checking name: 鈴木花子

PotentialAssignment.forValue() の第一引数はパラメータに対するラベルになり、例えば「鈴木花子」でテストに失敗すると以下のように表示される。

org.junit.experimental.theories.internal.ParameterizedAssertionError: テスト(名前2, 名字4)
...

テストパラメータの指定は他に @DataPoint, @DataPoints で行う方法もあるが、こちらは解説しているサイトが結構ありそうなので省略。

Theories は Parameterized と使いどころが似ているが、「大量のパラメータ全てに対して同じ結果となる理論(theory)=テスト対象ロジック」を確認する場合に使うもので、「色々なパラメータが様々な結果となる」場合は Parameterized の方が使いやすいと思われる。*3

*1:厳密には suite() メソッドが無いとか他にも条件がある

*2:今はこの2つは全く同じだが、将来的に中身が変わり JUnit4 クラスが標準になる可能性があるため

*3:Assume を使うことで条件付の Theory 確認もできるが、そこまでいくとテスト自体にロジックが入りすぎという気がする