mike-neckのブログ

Java or Groovy or Swift or Golang

JUnit5入門(6) - JUnit5とGuiceでDIが必要なオブジェクトを構築してテストする

前回の続き

これまでの内容をまとめて、DIが必要なオブジェクトをテストするやり方(のひとつ)をやってみる。DIにはGuiceを用いる。


環境

JDK|1.8.0_102 junit-jupiter-api|5.0.0-M3 junit-platform-engine|1.0.0-M3 guice|4.1.0


テスト対象のインターフェース

次のようなインターフェース/実装クラスをテストする。

インターフェース
public interface DateService {

  Locale getLocale();

  String date(int year, int month, int day);

  String today();

  String time(int hour, int minutes);

  String now();

  ZoneId getZone();
}
実装クラス
public class DateServiceImpl implements DateService {

  private final ZoneId zone;
  private final DateTimeFormatter dateFormat;
  private final DateTimeFormatter timeFormat;
  private final Locale locale;

  @Inject
  public DateServiceImpl(
      ZoneId zone
      , @Date DateTimeFormatter dateFormat
      , @Time DateTimeFormatter timeFormat
      , Locale locale
  ) {
    this.zone = zone;
    this.dateFormat = dateFormat.withLocale(locale);
    this.timeFormat = timeFormat.withLocale(locale);
    this.locale = locale;
  }

  @Override
  public Locale getLocale() {
    return locale;
  }
  @Override
  public String date(int year, int month, int day) {
    return LocalDate.of(year, month, day).format(dateFormat);
  }
  @Override
  public String today() {
    return LocalDate.now(zone).format(dateFormat);
  }
  @Override
  public String time(int hour, int minutes) {
    return LocalTime.of(hour, minutes).format(timeFormat);
  }
  @Override
  public String now() {
    return LocalDateTime.now(zone).format(timeFormat);
  }
  @Override
  public ZoneId getZone() {
    return zone;
  }
}

@Date@Timeバインディングアノテーション


テスト

guiceでオブジェクトを構築する ExtensionDateServiceGuiceExtension と名前をつけるとして、テストは次のような感じにする。

@DisplayName("DateServiceのテスト - 実装はDateServiceImpl")
public class DateServiceTest {
  @Nested
  @DisplayName("ロケールはen日付はMMM/d/uu,時刻はhh:mm,America/Los_Angeles")
  @Lang("en")
  @Date.Format("MMM/d/uu")
  @Time.Format("hh:mm")
  @Zone("America/Los_Angeles")
  @ExtendWith({ DateServiceGuiceExtension.class })
  class LosAngelesTest {
    @Test
    @DisplayName("dateの最初の3文字は英字")
    void first3CharactersIsAlphabet(DateService service) {
      final String today = service.today();
      assertTrue(today.substring(0, 3).matches("\\p{Alpha}{3}"));
    }
    @Test
    @DisplayName("時刻は必ず2文字ある")
    void hourHasTwoCharacters(DateService service) {
      final String now = service.now();
      assertEquals(2, now.split(":")[0].length());
    }
    @Test
    @DisplayName("タイムゾーンはAsia/Tokyoではない")
    void timeZoneIsNotAsiaTokyo(DateService service) {
      final ZoneId zone = service.getZone();
      assertNotEquals(of("Asia/Tokyo"), zone);
    }
  }
  @Nested
  @DisplayName("ロケール指定なし(デフォルト)/Dateの指定なし(uuuu/MM/dd)/Timeの指定なし(HH:mm:ss.SSS)/Zoneの指定なし(UTC)")
  @ExtendWith({ DateServiceGuiceExtension.class })
  class DefaultInjection {
    @Test
    @DisplayName("日付のフォーマットの確認")
    void validateDateFormat(DateService service) {
      assertAll(
          () -> assertEquals("2017/01/01", service.date(2017, 1, 1))
          , () -> assertEquals("2016/11/11", service.date(2016, 11, 11))
      );
    }
    @Test
    @DisplayName("時刻のフォーマットの確認")
    void validateTimeFormat(DateService service) {
      assertAll(
          () -> assertEquals("04:00:00.000", service.time(4, 0))
          , () -> assertEquals("13:13:00.000", service.time(13, 13))
      );
    }
  }
}

Extension の実装

ExtensionBeforeAllCallbackAfterAllCallbackParameterResolver を実装します。

  • BeforeAllCallback にてテストクラスの情報から必要な Injector を生成します。
  • ParameterResolver でテストが必要としているオブジェクトを解決します。
  • AfterAllCallback にて Injector を破棄します。
BeforeAllCallback/AfterAllCallback の実装
public abstract class GuiceExtension implements
    ParameterResolver
    , BeforeAllCallback
    , AfterAllCallback
{
  private static final Namespace GUICE = Namespace.create(com.example.guice.GuiceExtension.class);
  private static Store getStore(ExtensionContext context) {
    return context.getStore(GUICE);
  }

  abstract protected Module prepareModule(
      @NotNull Class<?> testClass
      , @NotNull Set<String> tags
      , @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
        @NotNull 
            Optional<AnnotatedElement> annotations
  );

  @Override
  public void beforeAll(ContainerExtensionContext context) throws Exception {
    final Store store = getStore(context);
    final Optional<AnnotatedElement> annotations = context.getElement();
    context.getTestClass()
        .map(c -> prepareModule(c, context.getTags(), annotations))
        .map(Guice::createInjector)
        .ifPresent(i -> store.put(Injector.class, i));
  }

  @Override
  public void afterAll(ContainerExtensionContext context) throws Exception {
    getStore(context).remove(Injector.class, Injector.class);
  }
}

このクラスを継承するクラスは、テストクラスに与えられたメタ情報(タグ/アノテーション)情報を元に、 Module を作ります。このクラスは Module から Injector を作りコンテキストに保持します。JUnit5のドキュメントによると Extensionインスタンスはテスト実行中に1つしか作られないので、テストの状態は Extension のフィールドには持たせずに Context が保持する Store に保持させるようにします。

ParameterResolver の実装

Store からテストクラスをキーにして Injector を取得し、さらにパラメーターの型、パラメーターに付与されたバインディングアノテーションをキーにしてインスタンスを取得します。

@Override
public boolean supports(
    ParameterContext parameterContext
    , ExtensionContext extensionContext
) throws ParameterResolutionException {
  final Parameter parameter = parameterContext.getParameter();
  final Class<?> paramType = parameter.getType();
  final Stream<? extends Key<?>> keyStream = Arrays
      .stream(parameter.getAnnotations())
      .map(a -> Key.get(paramType, a));
  return extensionContext.getTestClass()
      .map(c -> getStore(extensionContext).get(c, Injector.class))
      .map(Injector::getAllBindings)
      // a -> (a,a)
      .map(Pair.createPair(Function.identity()))
      // (a -> Bool) -> (b -> Bool) -> (a,b) -> Bool
      .filter(Pair.orFilterPair(
          b -> b.containsKey(Key.get(paramType))
          , b -> keyStream.anyMatch(b::containsKey)
      )).isPresent();
}
@Override
public Object resolve(
    ParameterContext parameterContext
    , ExtensionContext extensionContext
) throws ParameterResolutionException {
  final Parameter parameter = parameterContext.getParameter();
  final Class<?> paramType = parameter.getType();
  final Annotation[] annotations = parameter.getAnnotations();
  return extensionContext.getTestClass()
      .map(c -> getStore(extensionContext).get(c, Injector.class))
      .map(Injector::getAllBindings)
      // a -> (a, b)
      .map(Pair.createPair(b -> b.get(Key.get(paramType))))
      // (a, b) -> (b -> Maybe b) -> (a, Maybe b)
      .map(Pair.mapPair(Optional::ofNullable))
      // (a, Maybe b) -> (b -> c) -> (a, Maybe c)
      .map(Pair.mapPair(o -> o.map(Binding::getProvider)))
      // (a, Maybe c) -> (c -> Maybe t) -> (a, Maybe (Maybe t))
      .map(Pair.mapPair(o -> o.map(p -> (Object) p.get())))
      .map(Pair.mapPair(o -> o.map(Optional::of)))
      // (a, Maybe (Maybe t)) -> (a -> Maybe t -> Maybe t) -> Maybe t
      .flatMap(Pair.transformPair((m, o) -> o.orElseGet(
          // returns Optional<Object>
          findBindingByClassAndAnnotation(paramType, annotations, m))))
      .orElseThrow(() -> new ParameterResolutionException(
          String.format(
              "System cannot find binding for type: [%s] parameter index: %d"
              , parameter.getType()
              , parameterContext.getIndex())
      ));
}

これでテストを実行すると次のようになります。

f:id:mike_neck:20170103205333p:plain


なお、サンプルコードはこちらにあります。

github.com

JUnit入門(5) - JUnit5の拡張(Extension)

前回の続き

JUnitExtension インターフェース

JUnit4のRunnerと同様、JUnit5も拡張をすることができる。

JUnit5が提供している 拡張インターフェース(Extension を継承したインターフェース)は次のとおり。

  • ContainerExecutionCondition
    • メソッド - evaluate(ContainerExtensionContext)
      • 戻り値の型 - ConditionEvaluationResult
    • テストクラスレベルで実行可否の判断する
  • TestExecutionCondition
    • メソッド - evaluate(TestExtensionContext)
      • 戻り値の型 - ConditionEvaluationResult
    • テストメソッドの実行可否の判断する
  • BeforeAllCallback
    • メソッド - beforeAll(ContainerExtensionContext)
      • 戻り値の型 - void
    • @BeforeAll の前に実行される
  • BeforeEachCallback
    • メソッド - beforeEach(TestExtensionContext)
      • 戻り値の型 - void
    • @BeforeEach の前に実行される
  • BeforeTestExecutionCallback
    • メソッド - beforeTestExecution(TestExtensionContext)
      • 戻り値の型 - void
    • テスト(@Test)の前に実行される
  • AfterTestExecutionCallback
    • メソッド - afterTestExecution(TestExtensionContext)
      • 戻り値の型 - void
    • テスト(@Test)の後に実行される
  • AfterEachCallback
    • メソッド - afterEach(TestExtensionContext)
      • 戻り値の型 - void
    • @AfterEach` の後に実行される
  • AfterAllCallback
    • メソッド - afterAll(ContainerExtensionContext)
      • 戻り値の型 - void
    • @AfterAll の後に実行される
  • TestInstancePostProcessor
    • メソッド - postProcessTestInstance(Object, ExtensionContext)
      • 戻り値の型 - void
    • テストクラスのインスタンス生成後に実行される
  • TestExecutionExceptionHandler
    • メソッド - handleTestExecutionException(TestExtensionContext, Throwable)
      • 戻り値の型 - void
    • テスト時に発生した例外の扱いを決定する
  • ParameterResolver
    • メソッド - supports(ParameterContext, ExtensionContext)
      • 戻り値の型 - boolean
    • メソッド - resolve(ParameterContext, ExtensionContext)
      • 戻り値の型 - Object
    • テストメソッドに与える引数を解決する

テストライフサイクル

これらの Extension は次の順番でテストクラスの生成、テストの実行、テスト終了までの一連の流れに組み込まれる。

  1. ContainerExecutionCondition
  2. BeforeAllCallback
  3. テストクラスの @BeforeAll
  4. テストクラスのインスタンス生成
  5. TestInstancePostProcessor

ここから、テストがある場合

  1. TestExecutionCondition
  2. BeforeEachCallback
  3. テストクラスの @BeforeEach
  4. BeforeTestExecutionCallback
  5. ParameterResolver#supports(引数の数だけ)
  6. ParameterResolver#resolve(引数の数だけ)
  7. テストクラスの @Test(テスト)
  8. TestExecutionExceptionHandler(テスト中に例外が発生した場合だけ)
  9. AfterTestExecutionCallback
  10. テストクラスの @AfterEach
  11. AfterEachCallback

ここから、同じクラスに他のテストがある場合

  1. 次のテストのインスタンス生成
  2. TestInstancePostProcessor
  3. TestExecutionCondition
  4. ...
  5. AfterEachCallback

ここから、ネストしたクラスがある場合

  1. ContainerExecutionCondition
  2. BeforeAllCallback
  3. アウタークラスのインスタンス生成
  4. TestInstancePostProcessor
  5. インナークラスのインスタンス生成
  6. TestInstancePostProcessor
  7. TestExecutionCondition
  8. BeforeEachCallback
  9. アウタークラスの @BeforeEach
  10. インナークラスの @BeforeEach
  11. BeforeTestExecutionCallback
  12. ParameterResolver#supports(引数の数だけ)
  13. ParameterResolver#resolve(引数の数だけ)
  14. テストクラスの @Test(テスト)
  15. TestExecutionExceptionHandler(テスト中に例外が発生した場合だけ)
  16. AfterTestExecutionCallback
  17. インナークラスの @AfterEach
  18. アウタークラスの @AfterEach
  19. AfterEachCallback

ここから、インナークラスにテストがない場合

  1. AfterAllCallback

ここから、他にテストがない場合

  1. テストクラスの @AfterAll
  2. AfterAllCallback

確認コードはこちらにある、 NestedClasses.java/NoTest.java/TestLifecycleCallbacks.java を参照。

github.com


Extension の登録

実装した Extension@ExtendWith アノテーションをクラスまたはメソッドに指定することによって登録できる。

@ExtendWith({ FooExtension.class, BarExtension.class })
public class FooTest {
  @Test
  @ExtendWith(BazExtension.class)
  void qux() {}
}

なお、残念なことにjunit-jupiter-apiにはデフォルトで提供されている Extension はない。したがって、JUnit4で ExternalResource とか TestName を使っていた場合は、自分で Extension を書かなければならない(まあ、誰かがそのうち作ってくれるだろうけど…)。

各インターフェースの詳細

ContainerExecutionCondition/TestExecutionCondition

テストコンテナ(テストクラス)/テスト(テストメソッド)の条件に基づき実行制御を判断するメソッドを提供するインターフェース。それぞれ ContainerExecutionContext および TestExtensionContext を引数にとって、 ConditionEvaluationResult を返す。 ContainerEvaluationResult はstaticメソッド enabled(String)(テストを実行する)または disabled(テストを実行しない)によって生成できるオブジェクト。アノテーション @Disabled によってテストの実行制御を行っているのも、このインターフェースを実装した DisabledConditionによる。

サンプルコード : 指定した曜日しか動かないテスト

github.com

テスト中のサービスの起動状態によって、モックを使ったテストを実行する/テスト中のサービスでテストを実行するなどの切り替えに使えるかもしれない(それなら Condition でやるよりも ParameterResolver の方がよいけど…)。


Callback インターフェース

テストの各種フェーズをhookにして起動できる処理を定義する。起動される順序は上述のとおり。またJUnit5のドキュメントに書かれているようにテストの実行時間を計測したい場合は、テストクラスの @BeforeEach/@AfterEach ではなく @BeforeTestExecutionCallback および @AfterTestExecutionCallback によって計測するほうがより正確な実行時間が計測できる。


例外ハンドリング

TestExecutionExceptionHandler によって例外発生時の処理を挟み込むことができる。まあ、特定の例外は無視するくらいの使いみちしかないと思われるが…

TestInstancePostProcessor

テストクラスのインスタンスを起動した後に、特定の処理をおこないたい場合やDI、モックの準備をしたい場合などに作る。

サンプルコード

@PostConstruct アノテーションが付与されているメソッドを起動する TestInstancePostProcessor

public class PostProcessor implements TestInstancePostProcessor {

  private static final Predicate<Method> annotatedWithPostConstruct =
      m -> m.isAnnotationPresent(PostConstruct.class);

  @NotNull
  private static Consumer<Method> invokePostConstruct(
      @NotNull Object o
  ) {
    return m -> {
      try {
        m.setAccessible(true);
        m.invoke(o);
      } catch (IllegalAccessException | InvocationTargetException e) {
        final String name = m.getName();
        throw new IllegalStateException(name + " method cannot be invoked.", e);
      }
    };
  }

  @Override
  public void postProcessTestInstance(
      Object test
      , ExtensionContext context
  ) throws Exception {
    Stream.of(test.getClass().getDeclaredMethods())
        .filter(annotatedWithPostConstruct)
        .findAny()
        .ifPresent(invokePostConstruct(test));
  }
}

サンプルコード続きはこちら

github.com


ParameterResolver

ParameterResolver はテストメソッドの引数を解決するインターフェース。 support メソッドで引数を渡すことができるか判断し、引数を渡せる場合に resolve メソッドで実際の引数を渡す。メソッドに引数を渡せると聞くと、それ自身がテストのassertionの対象となる値を渡せると捉えがちだが、どちらかというとテストに関するメタ情報や特別な処理が必要になるオブジェクト(DI管理のオブジェクトやDIの対象となるオブジェクト、モックオブジェクトなど)を引数にとるようにする。(テストのためのパラメーターについては、 DynamicTest で作ればよく、テストのメソッドの引数にはそのパラメーターを作るためのオブジェクト(例えば File など)を渡した方がよい)。

サンプルコード

@InputString アノテーションを付与した String 型の引数に @InputString のパラメーターで指定した文字列を渡す。

github.com

JUnit入門(4) - TestFactoryとDynamicTest - JUnit5におけるパラメタライズドテスト実現法

前回の続き

@TestFactory

@TestFactory はJUnit4における Theories ランナーを用いたテストパラメーターをいろいろと組み合わせるテストを実現するために用いる。

@TestFactory を付与したメソッドは次のいずれかを返すメソッドを実装する必要がある。

  • Iterable<DynamicTest>
  • Iterator<DynamicTest>
  • Stream<DynamicTest>

DynamicTest はテストの名前とテストを持つクラスで、 DynamicTest#dynamicTest(String, Executable) から作ることができる。DynamicTestインスタンス一つにつきテスト1つとしてカウントされる。なお、javadocを読むかぎり @Test を付与したテストとは違い、 DynamicTest の実行の前後に @BeforeEach@AfterEach は実行されない。

サンプルコード
public class DynamicTestSample {

  @TestFactory
  Iterable<DynamicTest> dynamicTestIterable() {
    return Arrays.asList(
            dynamicTest("1 + 2 = 3", () -> assertEquals(3, 1 + 2))
            , dynamicTest("1 - 2 = -1", () -> assertEquals(-1, 1 - 2))
    );
  }

  @TestFactory
  Stream<DynamicTest> dynamicTestStream() {
    return Stream.of(
            new ThreeInts(1, 2, 3)
            , new ThreeInts(1, -2, -1)
    ).map(t -> dynamicTest(t.getTitle(), t.getTest()));
  }

  static class ThreeInts {
    final int left;
    final int right;
    final int result;

    ThreeInts(int left, int right, int result) {
      this.left = left;
      this.right = right;
      this.result = result;
    }

    String getTitle() {
      final String c = right < 0 ? "%d - %d = %d" : "%d + %d = %d";
      return String.format(c, left, Math.abs(right), result);
    }

    Executable getTest() {
      return () -> assertEquals(result, left + right);
    }
  }
}
実行結果
[         4 containers found      ]
[         0 containers skipped    ]
[         4 containers started    ]
[         0 containers aborted    ]
[         4 containers successful ]
[         0 containers failed     ]
[         4 tests found           ]
[         0 tests skipped         ]
[         4 tests started         ]
[         0 tests aborted         ]
[         4 tests successful      ]
[         0 tests failed          ]

@TestFactory のメソッドは2つだが、それぞれが2つずつテストを返して合計4つのテストが実行されている。


JUnit4で Theories ランナーで @DataPoints でデータを作っていた部分が、 DynamicTest 列の生成に置き換わり、 @Theory で行っていたテストそのものが DynamicTest に渡す Executable に置き換わったと考えるとわかりやすいかもしれません。


参考

github.com


続く

JUnit5入門(3) - アノテーション

前回の続き

JUnit4同様、JUnit5ではテストのメタ情報や、実行制御はアノテーションを介して行われる。今回はそのアノテーションをまとめる。

JUnit5が提供するアノテーション一覧

アノテーション 役割 説明
@Test メタ情報 付与されたメソッドがテストであることを示す
@TestFactory メタ情報 付与されたメソッドが返す Iterable などがテストであることを示す
@DisplayName メタ情報 テストの表示名を付与する
@Nested メタ情報 テストクラスのインナーテストクラスであることを示す
@Tag 実行制御 テストにタグを付与して
@Tags 実行制御 複数の @Tag をまとめる
@Disabled 実行制御 付与されたテスト/テストクラスは実行されない
@BeforeAll メソッド実行順 付与されたstaticメソッドはテストクラスで最初に実行される
@BeforeEach メソッド実行順 付与されたメソッドは各テスト/インナーテストクラスが実行される前に実行される
@AfterEach メソッド実行順 付与されたメソッドは各テスト/インナーテストクラスが実行された後に実行される
@AfterAll メソッド実行順 付与されたstaticメソッドはテストクラスで最後に実行される

アノテーション詳細

@Test は既に説明済み、 @TestFactory は後ほど説明するとして、その他のアノテーションについて触れておく。

@DisplayName

テストの表示名を設定する。これまでもテスト名でキャメルケースを活用したり、日本語名を用いたりしてテストの内容を説明できるようにしてきたが、メソッド名はシンプルな名前にしておいて、 @DisplayName でよりわかりやすい名前をつけたり、名前に半角ブランクをつけられるようになった。

サンプルコード
@Test
@DisplayName("This test checks int value.")
void checkingInt() {
  assertEquals(2, asInt()); // わざとfail
}
@Test
@DisplayName("This test checks double value.")
void checkingDouble() {
  assertEquals(65535/65536.0, asDouble(), 1/128.0);
}

private static final int ONE = 1;
private static int asInt() {
  return ONE;
}
private static double asDouble() {
  return ONE;
}
実行結果
Failures (1):
  JUnit Jupiter:表示名を変えるテスト:This test checks int value.
    MethodSource [className = 'com.example.ex3.DisplayNameTest', methodName = 'checkingInt', methodParameterTypes = '']
    => org.opentest4j.AssertionFailedError: expected: <2> but was: <1>

また、このようなこともできる。

@Test
@DisplayName("This test checks\n" +
    "int value.")
void checkingInt() {
  assertEquals(2, asInt()); // わざとfail
}
Failures (1):
  JUnit Jupiter:表示名を変えるテスト:This test checkes
int value
    MethodSource [className = 'com.example.ex3.DisplayNameTest', methodName = 'checkingInt', methodParameterTypes = '']
    => org.opentest4j.AssertionFailedError: expected: <2> but was: <1>

テスト名に改行を入れることができるので、次のようにテストに関するドキュメントを記述しておくことが可能になる(ただしこの例はkotlinだが…)。

@Test
@DisplayName("""
文字列のチェック
---

このテストは次のチェックを行う

* 小文字にしたときに同じ文字列であること
* 文字列の長さが同じであること
""")
fun multipleLineDisplayName() {
  String japan = "JAPAN";
  assertAll(
      () -> assertEquals("japan", japan.toLowerCase())
      , () -> assertEquals(5, japan.length())
  );
}

@Nested

複数のテストをまとめて見通しを良くするために Enclosed.class ランナーを用いてインナークラスで整理する手法はJUnit4でもあったが、JUnit5でも利用できる。ただし、JUnit4ではインナークラスはstaticクラスである必要があったのに対して、JUnit5ではstaticクラスを使うことができなくなった。

サンプルコード
@Slf4j
@DisplayName("トップ")
public class NestedClasses {

  @Test
  @DisplayName("トップのテスト")
  void test() {
    log.info("トップのテスト");
  }

  @Nested
  @DisplayName("ミドル")
  class MiddleInner {

    @Test
    @DisplayName("ミドルにあるテスト")
    void test() {
      log.info("ミドルにあるテスト");
    }

    @Nested
    @DisplayName("ボトム")
    class MostInner {

      @Test
      @DisplayName("ボトムのテスト")
      void test() {
        log.info("ボトムのテスト");
      }
    }
  }
}
実行結果
21:24:43.608 [INFO  com.example.ex4.NestedClasses] - トップのテスト
21:24:43.630 [INFO  com.example.ex4.NestedClasses] - ミドルにあるテスト
21:24:43.659 [INFO  com.example.ex4.NestedClasses] - ボトムのテスト

なお、IntelliJで実行すると…

f:id:mike_neck:20170101214329p:plain

@DisplayName が省略される

実行順に関するアノテーション

@BeforeAll などのアノテーションが付与されたテストは次の順序で実行される。

  1. @BeforeAll が付与されたstaticメソッド
  2. @BeforeEach が付与されたメソッド
  3. @Test または @TestFactory が付与されたメソッド
  4. @AfterEach が付与されたメソッド
  5. @BeforeEach が付与されたメソッド
  6. @Test または @TestFactory ...
  7. ...
  8. @AfterEach が付与されたメソッド
  9. @AfterAll が付与されたstaticメソッド

JUnit4のアノテーションと対応は次のようになる。

JUnit4 JUnit5
@BeforeClass @BeforeAll
@Before @BeforeEach
@After @AfterEach
@AfterClass @AfterAll
サンプルコード
@Slf4j
public class ExecutionModel {

  @BeforeAll
  static void beforeAll() throws InterruptedException {
    log.info("beforeAll");
  }

  @AfterAll
  static void afterAll() throws InterruptedException {
    log.info("afterAll");
  }

  @BeforeEach
  void beforeEach() throws InterruptedException {
    log.info("beforeEach");
  }

  @AfterEach
  void afterEach() throws InterruptedException {
    log.info("afterEach");
  }

  @Test
  void testFirst() throws InterruptedException {
    log.info("testFirst");
  }

  @Test
  void testSecond() throws InterruptedException {
    log.info("testSecond");
  }

  @Test
  void testThird() throws InterruptedException {
    log.info("testThird");
  }
}
実行結果
22:15:39.626 [INFO  com.example.ex3.ExecutionModel] - beforeAll
22:15:39.633 [INFO  com.example.ex3.ExecutionModel] - beforeEach
22:15:39.636 [INFO  com.example.ex3.ExecutionModel] - testFirst
22:15:39.642 [INFO  com.example.ex3.ExecutionModel] - afterEach
22:15:39.649 [INFO  com.example.ex3.ExecutionModel] - beforeEach
22:15:39.649 [INFO  com.example.ex3.ExecutionModel] - testSecond
22:15:39.649 [INFO  com.example.ex3.ExecutionModel] - afterEach
22:15:39.651 [INFO  com.example.ex3.ExecutionModel] - beforeEach
22:15:39.651 [INFO  com.example.ex3.ExecutionModel] - testThird
22:15:39.651 [INFO  com.example.ex3.ExecutionModel] - afterEach
22:15:39.653 [INFO  com.example.ex3.ExecutionModel] - afterAll

@Disabled

@Disabled はクラスまたはメソッドに付与してテストを実行させないようにすることができる。JUnit4における @Ignore に相当する。

@DisplayName("除外するテストの例")
public class DisablingTests {

  @Test
  @Disabled
  @DisplayName("動かさないテスト")
  void thisTestWillFail() {
    fail("動かさないテスト");
  }

  @Test
  @DisplayName("動かすテスト")
  void thisTestCanBeRunnable() {
    assertEquals(1, 1);
  }

  @Nested
  @DisplayName("動かすインナークラス")
  class WorkingInner {

    @Test
    @Disabled
    @DisplayName("動かさないテスト")
    void notWorking() {
      fail("not working now.");
    }

    @Test
    @DisplayName("動かすテスト")
    void working() {
      assertTrue(true);
    }
  }

  @Nested
  @Disabled
  @DisplayName("動かさないインナークラス")
  class NotWorking {

    @Test
    @Disabled
    @DisplayName("動かさないテスト")
    void notWorking() {
      fail("not working now.");
    }

    @Test
    @DisplayName("動かすテスト")
    void working() {
      assertTrue(true);
    }
  }
}
実行結果
[         4 containers found      ]
[         1 containers skipped    ]
[         3 containers started    ]
[         0 containers aborted    ]
[         3 containers successful ]
[         0 containers failed     ]
[         6 tests found           ]
[         4 tests skipped         ]
[         2 tests started         ]
[         0 tests aborted         ]
[         2 tests successful      ]
[         0 tests failed          ]

親クラスのテストが2つ(@Disabled 1つ)、 実行する方の子クラスのテストが2つ(@Disabled 1つ)、実行しない(@Disabled)子クラスのテストが2つの合計6つテストがあり、スキップされたのが @Disabled が付与された4つ(1 + 1 + 2)であり、実行されたテストは2つであることが結果からもわかる。

@Tag/@Tags

@Tag は実行制御に関するアノテーションと書いたが、実際の所テストにメタ情報をつけるだけで、IDEから実行する際には特に意味がない(今後タグを識別して実行するようになるかもしれないが…)。gradleなどのplatformからテストを実行する場合にタグ情報をテスト実行制御に用いる。JUnit4における @Categoies に相当する。

サンプルコード
@Slf4j
@DisplayName("タグをつけたテスト")
public class TaggedTests {

  @Test
  @Tag("one")
  @DisplayName("1st というタグを付けたテスト(タグはIDEで実行する上では特に意味がない)")
  void test1st() {
    log.info("1st");
  }

  @Test
  @Tag("two")
  @DisplayName("two という タグ")
  void test2nd() {
    log.info("2nd");
  }

  @Test
  @Tags({
      @Tag("one")
      , @Tag("two")
      , @Tag("three")
  })
  @DisplayName("Tagsでタグたくさん")
  void test3rd() {
    log.info("3rd");
  }

  @Test
  @Tag("one")
  @Tag("three")
  @Tag("this is four")
  @DisplayName("Tagsの中にではなく、Tagをたくさん")
  void test4th() {
    log.info("4th");
  }

  @Test
  @Tag("two")
  @Tag("three")
  @Tag("this is five")
  @DisplayName("Tagsの中にではなく、Tagをたくさん")
  void test5th() {
    log.info("5th");
  }
}
実行結果
22:37:14.794 [INFO  com.example.ex5.TaggedTests] - 1st
22:37:14.806 [INFO  com.example.ex5.TaggedTests] - 3rd
22:37:14.808 [INFO  com.example.ex5.TaggedTests] - 2nd
22:37:14.810 [INFO  com.example.ex5.TaggedTests] - 4th
22:37:14.813 [INFO  com.example.ex5.TaggedTests] - 5th

特に設定をしない限りはすべてのテストが実行される。

JUnit5で @Tag の値に基づき実行を制御するためには、 build.gradlejunitPlatform クロージャー中に次のような記述を行う。

例(twoタグを除外する場合)
junitPlatform {
  filters {
    tags {
      exclude 'two'
    }
  }
}
実行結果
22:59:23.964 [INFO  com.example.ex5.TaggedTests] - 1st
22:59:23.973 [INFO  com.example.ex5.TaggedTests] - 4th

Test run finished after 10321 ms
[         2 containers found      ]
[         0 containers skipped    ]
[         2 containers started    ]
[         0 containers aborted    ]
[         2 containers successful ]
[         0 containers failed     ]
[         2 tests found           ]
[         0 tests skipped         ]
[         2 tests started         ]
[         0 tests aborted         ]
[         2 tests successful      ]
[         0 tests failed          ]

@Tag("two") が付与された test2ndtest3rdtest5th は実行されず、またテストとしてもカウントされてないことに注意したい。

例(excludeinclude を指定した場合)
junitPlatform {
  filters {
    tags {
      exclude 'two'
      include 'three'
    }
  }
}
実行結果
23:04:13.315 [INFO  com.example.ex5.TaggedTests] - 4th

two が付与されておらず three が付与されている test4th が実行された。

サンプルコードはこちら

github.com

github.com

github.com


つづく