mike-neckのブログ

Java or Groovy or Swift or Golang

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