1. 概要

JUnit 5 ライブラリは、以前のバージョンに比べて多くの新機能を提供します。 そのような機能の1つは、テストテンプレートです。 要するに、テストテンプレートは、JUnit5のパラメーター化された繰り返しテストの強力な一般化です。

このチュートリアルでは、JUnit5を使用してテストテンプレートを作成する方法を学習します。

2. Mavenの依存関係

pom.xmlに依存関係を追加することから始めましょう。

メインのJUnit5junit-jupiter-engine依存関係を追加する必要があります。

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.8.1</version>
</dependency>

これに加えて、junit-jupiter-api依存関係も追加する必要があります。

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.8.1</version>
</dependency>

同様に、必要な依存関係をbuild.gradleファイルに追加できます。

testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.8.1'
testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.8.1'

3. 問題の説明

テストテンプレートを見る前に、JUnit5のパラメーター化されたテストを簡単に見てみましょう。 パラメータ化されたテストにより、テストメソッドにさまざまなパラメータを挿入できます。 その結果、パラメータ化されたテストを使用する場合、異なるパラメータを使用して単一のテストメソッドを複数回実行できます。

ここで、テストメソッドを複数回実行したいとします。パラメーターが異なるだけでなく、呼び出しコンテキストも毎回異なります。

言い換えると、テストメソッドを複数回実行し、呼び出しごとに次のような構成の異なる組み合わせを使用する必要があります。

  • さまざまなパラメータを使用する
  • テストクラスインスタンスの準備を変える—つまり、テストインスタンスに異なる依存関係を挿入する
  • 環境が「QA」の場合に呼び出しのサブセットを有効/無効にするなど、さまざまな条件下でテストを実行する
  • 異なるライフサイクルコールバック動作で実行している—おそらく、呼び出しのサブセットの前後にデータベースをセットアップして破棄したい

この場合、パラメータ化されたテストの使用はすぐに制限されます。 ありがたいことに、JUnit 5は、テストテンプレートの形式でこのシナリオの強力なソリューションを提供します。

4. テストテンプレート

テストテンプレート自体はテストケースではありません。 代わりに、それらの名前が示すように、それらは特定のテストケースの単なるテンプレートです。 これらは、パラメーター化された繰り返しテストの強力な一般化です。

テストテンプレートは、呼び出しコンテキストプロバイダーによって提供された呼び出しコンテキストごとに1回呼び出されます。

次に、テストテンプレートの例を見てみましょう。 上で確立したように、主なアクターは次のとおりです。

  • テストターゲットメソッド
  • テストテンプレートメソッド
  • テンプレートメソッドに登録された1つ以上の呼び出しコンテキストプロバイダー
  • 各呼び出しコンテキストプロバイダーによって提供される1つ以上の呼び出しコンテキスト

4.1. テストターゲットメソッド

この例では、テストターゲットとして単純なUserIdGeneratorImpl.generateメソッドを使用します。

UserIdGeneratorImplクラスを定義しましょう。

public class UserIdGeneratorImpl implements UserIdGenerator {
    private boolean isFeatureEnabled;

    public UserIdGeneratorImpl(boolean isFeatureEnabled) {
        this.isFeatureEnabled = isFeatureEnabled;
    }

    public String generate(String firstName, String lastName) {
        String initialAndLastName = firstName.substring(0, 1).concat(lastName);
        return isFeatureEnabled ? "bael".concat(initialAndLastName) : initialAndLastName;
    }
}

テストターゲットであるgenerateメソッドは、firstNamelastNameをパラメーターとして受け取り、ユーザーIDを生成します。 ユーザーIDの形式は、機能スイッチが有効になっているかどうかによって異なります。

これがどのように見えるか見てみましょう:

Given feature switch is disabled When firstName = "John" and lastName = "Smith" Then "JSmith" is returned
Given feature switch is enabled When firstName = "John" and lastName = "Smith" Then "baelJSmith" is returned

次に、テストテンプレートメソッドを記述しましょう。

4.2. テストテンプレートメソッド

テストターゲットメソッドUserIdGeneratorImpl.generateのテストテンプレートは次のとおりです。

public class UserIdGeneratorImplUnitTest {
    @TestTemplate
    @ExtendWith(UserIdGeneratorTestInvocationContextProvider.class)
    public void whenUserIdRequested_thenUserIdIsReturnedInCorrectFormat(UserIdGeneratorTestCase testCase) {
        UserIdGenerator userIdGenerator = new UserIdGeneratorImpl(testCase.isFeatureEnabled());

        String actualUserId = userIdGenerator.generate(testCase.getFirstName(), testCase.getLastName());

        assertThat(actualUserId).isEqualTo(testCase.getExpectedUserId());
    }
}

テストテンプレートメソッドを詳しく見てみましょう。

まず、 JUnit 5 @TestTemplateアノテーションでマークを付けて、テストテンプレートメソッドを作成します。

続いて、@ ExtendWithアノテーションを使用して、コンテキストプロバイダー UserIdGeneratorTestInvocationContextProvider、を登録します。 テストテンプレートに複数のコンテキストプロバイダーを登録できます。 ただし、この例では、単一のプロバイダーを登録します。

また、テンプレートメソッドはUserIdGeneratorTestCaseのインスタンスをパラメーターとして受け取ります。 これは、テストケースの入力と期待される結果のラッパークラスにすぎません。

public class UserIdGeneratorTestCase {
    private boolean isFeatureEnabled;
    private String firstName;
    private String lastName;
    private String expectedUserId;

    // Standard setters and getters
}

最後に、テストターゲットメソッドを呼び出し、その結果が期待どおりであることを表明します

次に、呼び出しコンテキストプロバイダーを定義します。

4.3. 呼び出しコンテキストプロバイダー

少なくとも1つのTestTemplateInvocationContextProviderをテストテンプレートに登録する必要があります。 登録された各TestTemplateInvocationContextProviderは、TestTemplateInvocationContextインスタンスのストリームを提供します

以前は、 @ExtendWith アノテーションを使用して、UserIdGeneratorTestInvocationContextProviderを呼び出しプロバイダーとして登録していました。

このクラスを今すぐ定義しましょう:

public class UserIdGeneratorTestInvocationContextProvider implements TestTemplateInvocationContextProvider {
    //...
}

呼び出しコンテキストは、TestTemplateInvocationContextProviderインターフェイスを実装します。これには2つのメソッドがあります。

  • supportsTestTemplate
  • ProvideTestTemplateInvocationContexts

supportsTestTemplateメソッドを実装することから始めましょう。

@Override
public boolean supportsTestTemplate(ExtensionContext extensionContext) {
    return true;
}

JUnit5実行エンジンは最初にsupportsTestTemplateメソッドを呼び出して、プロバイダーが特定のExecutionContextに適用可能かどうかを検証します。 この場合、単にtrueを返します。

それでは、ProvideTestTemplateInvocationContextsメソッドを実装しましょう。

@Override
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
  ExtensionContext extensionContext) {
    boolean featureDisabled = false;
    boolean featureEnabled = true;
 
    return Stream.of(
      featureDisabledContext(
        new UserIdGeneratorTestCase(
          "Given feature switch disabled When user name is John Smith Then generated userid is JSmith",
          featureDisabled,
          "John",
          "Smith",
          "JSmith")),
      featureEnabledContext(
        new UserIdGeneratorTestCase(
          "Given feature switch enabled When user name is John Smith Then generated userid is baelJSmith",
          featureEnabled,
          "John",
          "Smith",
          "baelJSmith"))
    );
}

ProvideTestTemplateInvocationContexts メソッドの目的は、TestTemplateInvocationContextインスタンスのStreamを提供することです。 この例では、メソッドfeatureDisabledContextおよびfeatureEnabledContextによって提供される2つのインスタンスを返します。 したがって、テストテンプレートは2回実行されます。

次に、これらのメソッドによって返される2つのTestTemplateInvocationContextインスタンスを見てみましょう。

4.4. 呼び出しコンテキストインスタンス

呼び出しコンテキストは、 TestTemplateInvocationContext インターフェイスの実装であり、次のメソッドを実装します。

  • getDisplayName –テスト表示名を指定します
  • getAdditionalExtensions –呼び出しコンテキストの追加の拡張機能を返します

最初の呼び出しコンテキストインスタンスを返すfeatureDisabledContextメソッドを定義しましょう。

private TestTemplateInvocationContext featureDisabledContext(
  UserIdGeneratorTestCase userIdGeneratorTestCase) {
    return new TestTemplateInvocationContext() {
        @Override
        public String getDisplayName(int invocationIndex) {
            return userIdGeneratorTestCase.getDisplayName();
        }

        @Override
        public List<Extension> getAdditionalExtensions() {
            return asList(
              new GenericTypedParameterResolver(userIdGeneratorTestCase), 
              new BeforeTestExecutionCallback() {
                  @Override
                  public void beforeTestExecution(ExtensionContext extensionContext) {
                      System.out.println("BeforeTestExecutionCallback:Disabled context");
                  }
              }, 
              new AfterTestExecutionCallback() {
                  @Override
                  public void afterTestExecution(ExtensionContext extensionContext) {
                      System.out.println("AfterTestExecutionCallback:Disabled context");
                  }
              }
            );
        }
    };
}

まず、 featureDisabledContext メソッドによって返される呼び出しコンテキストの場合、登録するエクステンションは次のとおりです。

  • GenericTypedParameterResolver パラメーターリゾルバー拡張
  • BeforeTestExecutionCallback –テスト実行の直前に実行されるライフサイクルコールバック拡張機能
  • AfterTestExecutionCallback –テスト実行の直後に実行されるライフサイクルコールバック拡張機能

ただし、 featureEnabledContext メソッドによって返される2番目の呼び出しコンテキストについては、別の拡張機能のセットを登録しましょう( GenericTypedParameterResolver を維持)。

private TestTemplateInvocationContext featureEnabledContext(
  UserIdGeneratorTestCase userIdGeneratorTestCase) {
    return new TestTemplateInvocationContext() {
        @Override
        public String getDisplayName(int invocationIndex) {
            return userIdGeneratorTestCase.getDisplayName();
        }
    
        @Override
        public List<Extension> getAdditionalExtensions() {
            return asList(
              new GenericTypedParameterResolver(userIdGeneratorTestCase), 
              new DisabledOnQAEnvironmentExtension(), 
              new BeforeEachCallback() {
                  @Override
                  public void beforeEach(ExtensionContext extensionContext) {
                      System.out.println("BeforeEachCallback:Enabled context");
                  }
              }, 
              new AfterEachCallback() {
                  @Override
                  public void afterEach(ExtensionContext extensionContext) {
                      System.out.println("AfterEachCallback:Enabled context");
                  }
              }
            );
        }
    };
}

2番目の呼び出しコンテキストの場合、登録する拡張子は次のとおりです。

  • GenericTypedParameterResolver –パラメーターリゾルバー拡張機能
  • DisabledOnQAEnvironmentExtension –環境プロパティ( application.properties ファイルからロードされた)が「qa」である場合にテストを無効にする実行条件
  • BeforeEachCallback –各テストメソッドの実行前に実行されるライフサイクルコールバック拡張機能
  • AfterEachCallback –各テストメソッドの実行後に実行されるライフサイクルコールバック拡張機能

上記の例から、次のことがわかります。

  • 同じテストメソッドが複数の呼び出しコンテキストで実行されます
  • 各呼び出しコンテキストは、他の呼び出しコンテキストの拡張機能とは数と性質の両方が異なる独自の拡張機能のセットを使用します

その結果、毎回完全に異なる呼び出しコンテキストでテストメソッドを複数回呼び出すことができます。 また、複数のコンテキストプロバイダーを登録することで、テストを実行するための呼び出しコンテキストのさらに多くのレイヤーを提供できます。

5. 結論

この記事では、JUnit5のテストテンプレートがパラメーター化された繰り返しテストの強力な一般化である方法について説明しました。

まず、パラメーター化されたテストのいくつかの制限について説明しました。 次に、呼び出しごとに異なるコンテキストでテストを実行できるようにすることで、テストテンプレートが制限を克服する方法について説明しました。

最後に、新しいテストテンプレートを作成する例を確認しました。 テンプレートが呼び出しコンテキストプロバイダーおよび呼び出しコンテキストと連携してどのように機能するかを理解するために、例を分解しました。

いつものように、この記事で使用されている例のソースコードは、GitHubから入手できます。