JUnit 5エクステンションの手引き

JUnit 5拡張機能のガイド

1. 概要

この記事では、JUnit5テストライブラリの拡張モデルを見ていきます。 名前が示すように、the purpose of Junit 5 extensions is to extend the behavior of test classes or methods、およびこれらは複数のテストに再利用できます。

Junit 5より前のJUnit 4バージョンのライブラリでは、テストを拡張するために、テストランナーとルールの2種類のコンポーネントが使用されていました。 比較すると、JUnit 5は、Extension APIという単一の概念を導入することにより、拡張メカニズムを簡素化します。

2. JUnit 5拡張モデル

JUnit 5の拡張機能は、テストの実行における特定のイベント(拡張ポイントと呼ばれる)に関連しています。 特定のライフサイクルフェーズに達すると、JUnitエンジンは登録済みの拡張機能を呼び出します。

5つの主なタイプの拡張ポイントを使用できます。

  • テストインスタンスの後処理

  • 条件付きテストの実行

  • ライフサイクルコールバック

  • パラメータ解決

  • 例外処理

次のセクションでは、これらのそれぞれについて詳しく説明します。

3. Mavenの依存関係

まず、例に必要なプロジェクトの依存関係を追加しましょう。 必要なメインのJUnit5ライブラリはjunit-jupiter-engineです。


    org.junit.jupiter
    junit-jupiter-engine
    5.4.2
    test

また、例で使用する2つのヘルパーライブラリも追加しましょう。


    org.apache.logging.log4j
    log4j-core
    2.8.2


    com.h2database
    h2
    1.4.196

junit-jupiter-engineh2、およびlog4j-coreの最新バージョンは、MavenCentralからダウンロードできます。

4. JUnit5拡張機能の作成

JUnit 5拡張機能を作成するには、JUnit 5拡張ポイントに対応する1つ以上のインターフェイスを実装するクラスを定義する必要があります。 これらのインターフェースはすべて、マーカーインターフェースにすぎないメインのExtensionインターフェースを拡張します。

4.1. TestInstancePostProcessor拡張子

このタイプの拡張機能は、テストのインスタンスが作成された後に実行されます。 実装するインターフェースはTestInstancePostProcessorで、これにはオーバーライドするpostProcessTestInstance()メソッドがあります。

この拡張機能の典型的な使用例は、インスタンスに依存関係を注入することです。 たとえば、loggerオブジェクトをインスタンス化する拡張機能を作成してから、テストインスタンスでsetLogger()メソッドを呼び出します。

public class LoggingExtension implements TestInstancePostProcessor {

    @Override
    public void postProcessTestInstance(Object testInstance,
      ExtensionContext context) throws Exception {
        Logger logger = LogManager.getLogger(testInstance.getClass());
        testInstance.getClass()
          .getMethod("setLogger", Logger.class)
          .invoke(testInstance, logger);
    }
}

上記のように、postProcessTestInstance()メソッドはテストインスタンスへのアクセスを提供し、リフレクションのメカニズムを使用してテストクラスのsetLogger()メソッドを呼び出します。

4.2. 条件付きテストの実行

JUnit 5は、テストを実行するかどうかを制御できる拡張機能を提供します。 これは、ExecutionConditionインターフェースを実装することによって定義されます。

このインターフェイスを実装し、evaluateExecutionCondition()メソッドをオーバーライドするEnvironmentExtensionクラスを作成しましょう。

このメソッドは、現在の環境名を表すプロパティが“qa”に等しいかどうかを確認し、この場合はテストを無効にします。

public class EnvironmentExtension implements ExecutionCondition {

    @Override
    public ConditionEvaluationResult evaluateExecutionCondition(
      ExtensionContext context) {

        Properties props = new Properties();
        props.load(EnvironmentExtension.class
          .getResourceAsStream("application.properties"));
        String env = props.getProperty("env");
        if ("qa".equalsIgnoreCase(env)) {
            return ConditionEvaluationResult
              .disabled("Test disabled on QA environment");
        }

        return ConditionEvaluationResult.enabled(
          "Test enabled on QA environment");
    }
}

その結果、この拡張機能を登録するテストは、“qa”環境では実行されません。

条件に一致するパターンへのIf we do not want a condition to be validated, we can deactivate it by setting the junit.conditions.deactivate configuration key

これは、-Djunit.conditions.deactivate=<pattern>プロパティを使用してJVMを起動するか、LauncherDiscoveryRequestに構成パラメーターを追加することで実現できます。

public class TestLauncher {
    public static void main(String[] args) {
        LauncherDiscoveryRequest request
          = LauncherDiscoveryRequestBuilder.request()
          .selectors(selectClass("com.example.EmployeesTest"))
          .configurationParameter(
            "junit.conditions.deactivate",
            "com.example.extensions.*")
          .build();

        TestPlan plan = LauncherFactory.create().discover(request);
        Launcher launcher = LauncherFactory.create();
        SummaryGeneratingListener summaryGeneratingListener
          = new SummaryGeneratingListener();
        launcher.execute(
          request,
          new TestExecutionListener[] { summaryGeneratingListener });

        System.out.println(summaryGeneratingListener.getSummary());
    }
}

4.3. ライフサイクルコールバック

この拡張機能のセットは、テストのライフサイクルのイベントに関連しており、次のインターフェースを実装することで定義できます。

  • BeforeAllCallbackおよびAfterAllCallback –すべてのテストメソッドが実行される前後に実行されます

  • BeforeEachCallBackおよびAfterEachCallback –各テストメソッドの前後に実行

  • BeforeTestExecutionCallbackおよびAfterTestExecutionCallback –テストメソッドの直前と直後に実行

テストでライフサイクルメソッドも定義されている場合、実行順序は次のとおりです。

  1. BeforeAllCallback

  2. BeforeAll

  3. BeforeEachCallback

  4. BeforeEach

  5. BeforeTestExecutionCallback

  6. Test

  7. AfterTestExecutionCallback

  8. AfterEach

  9. AfterEachCallback

  10. 結局

  11. AfterAllCallback

この例では、これらのインターフェースのいくつかを実装し、JDBCを使用してデータベースにアクセスするテストの動作を制御するクラスを定義しましょう。

まず、単純なEmployeeエンティティを作成しましょう。

public class Employee {

    private long id;
    private String firstName;
    // constructors, getters, setters
}

また、.propertiesファイルに基づいてConnectionを作成するユーティリティクラスも必要です。

public class JdbcConnectionUtil {

    private static Connection con;

    public static Connection getConnection()
      throws IOException, ClassNotFoundException, SQLException{
        if (con == null) {
            // create connection
            return con;
        }
        return con;
    }
}

最後に、Employeeレコードを操作する単純なJDBCベースのDAOを追加しましょう。

public class EmployeeJdbcDao {
    private Connection con;

    public EmployeeJdbcDao(Connection con) {
        this.con = con;
    }

    public void createTable() throws SQLException {
        // create employees table
    }

    public void add(Employee emp) throws SQLException {
       // add employee record
    }

    public List findAll() throws SQLException {
       // query all employee records
    }
}

いくつかのライフサイクルインターフェイスを実装する拡張機能を作成しましょう。

public class EmployeeDatabaseSetupExtension implements
  BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback {
    //...
}

これらの各インターフェイスには、オーバーライドする必要があるメソッドが含まれています。

BeforeAllCallbackインターフェイスの場合、beforeAll()メソッドをオーバーライドし、テストメソッドが実行される前にemployeesテーブルを作成するロジックを追加します。

private EmployeeJdbcDao employeeDao = new EmployeeJdbcDao();

@Override
public void beforeAll(ExtensionContext context) throws SQLException {
    employeeDao.createTable();
}

次に、BeforeEachCallbackAfterEachCallbackを使用して、トランザクション内の各テストメソッドをラップします。 これの目的は、テストメソッドで実行されたデータベースへの変更をロールバックして、次のテストがクリーンなデータベースで実行されるようにすることです。

beforeEach()メソッドでは、データベースの状態を次の場所にロールバックするために使用する保存ポイントを作成します。

private Connection con = JdbcConnectionUtil.getConnection();
private Savepoint savepoint;

@Override
public void beforeEach(ExtensionContext context) throws SQLException {
    con.setAutoCommit(false);
    savepoint = con.setSavepoint("before");
}

次に、afterEach()メソッドで、テストメソッドの実行中に行われたデータベースの変更をロールバックします。

@Override
public void afterEach(ExtensionContext context) throws SQLException {
    con.rollback(savepoint);
}

接続を閉じるには、すべてのテストが終了した後に実行されるafterAll()メソッドを使用します。

@Override
public void afterAll(ExtensionContext context) throws SQLException {
    if (con != null) {
        con.close();
    }
}

4.4. パラメータ解決

テストコンストラクターまたはメソッドがパラメーターを受け取る場合、これは実行時にParameterResolverによって解決される必要があります。

タイプEmployeeJdbcDaoのパラメータを解決する独自のカスタムParameterResolverを定義しましょう。

public class EmployeeDaoParameterResolver implements ParameterResolver {

    @Override
    public boolean supportsParameter(ParameterContext parameterContext,
      ExtensionContext extensionContext) throws ParameterResolutionException {
        return parameterContext.getParameter().getType()
          .equals(EmployeeJdbcDao.class);
    }

    @Override
    public Object resolveParameter(ParameterContext parameterContext,
      ExtensionContext extensionContext) throws ParameterResolutionException {
        return new EmployeeJdbcDao();
    }
}

リゾルバーはParameterResolverインターフェースを実装し、supportsParameter()メソッドとresolveParameter()メソッドをオーバーライドします。 これらの最初はパラメータの型を検証し、2番目はパラメータインスタンスを取得するロジックを定義します。

4.5. 例外処理

最後になりましたが、TestExecutionExceptionHandlerインターフェースを使用して、特定のタイプの例外が発生したときのテストの動作を定義できます。

たとえば、タイプFileNotFoundExceptionのすべての例外をログに記録して無視し、他のタイプを再スローする拡張機能を作成できます。

public class IgnoreFileNotFoundExceptionExtension
  implements TestExecutionExceptionHandler {

    Logger logger = LogManager
      .getLogger(IgnoreFileNotFoundExceptionExtension.class);

    @Override
    public void handleTestExecutionException(ExtensionContext context,
      Throwable throwable) throws Throwable {

        if (throwable instanceof FileNotFoundException) {
            logger.error("File not found:" + throwable.getMessage());
            return;
        }
        throw throwable;
    }
}

5. 拡張機能の登録

テスト拡張機能を定義したので、JUnit 5テストでそれらを登録する必要があります。 これを実現するために、@ExtendWithアノテーションを利用できます。

アノテーションはテストに複数回追加するか、パラメーターとして拡張機能のリストを受け取ることができます。

@ExtendWith({ EnvironmentExtension.class,
  EmployeeDatabaseSetupExtension.class, EmployeeDaoParameterResolver.class })
@ExtendWith(LoggingExtension.class)
@ExtendWith(IgnoreFileNotFoundExceptionExtension.class)
public class EmployeesTest {
    private EmployeeJdbcDao employeeDao;
    private Logger logger;

    public EmployeesTest(EmployeeJdbcDao employeeDao) {
        this.employeeDao = employeeDao;
    }

    @Test
    public void whenAddEmployee_thenGetEmployee() throws SQLException {
        Employee emp = new Employee(1, "john");
        employeeDao.add(emp);
        assertEquals(1, employeeDao.findAll().size());
    }

    @Test
    public void whenGetEmployees_thenEmptyList() throws SQLException {
        assertEquals(0, employeeDao.findAll().size());
    }

    public void setLogger(Logger logger) {
        this.logger = logger;
    }
}

テストクラスには、EmployeeDaoParameterResolver拡張機能を拡張することで解決されるEmployeeJdbcDaoパラメーターを持つコンストラクターがあることがわかります。

EnvironmentExtensionを追加することにより、テストは“qa”とは異なる環境でのみ実行されます。

このテストでは、employeesテーブルも作成され、各メソッドはEmployeeDatabaseSetupExtensionを追加してトランザクションにラップされます。 whenAddEmployee_thenGetEmploee()テストが最初に実行されてテーブルに1つのレコードが追加された場合でも、2番目のテストではテーブル内に0レコードが見つかります。

LoggingExtensionを使用して、ロガーインスタンスがクラスに追加されます。

最後に、テストクラスは、対応する拡張機能を追加しているため、すべてのFileNotFoundExceptionインスタンスを無視します。

5.1. 自動延長登録

アプリケーションのすべてのテストの拡張子を登録する場合は、完全修飾名を/META-INF/services/org.junit.jupiter.api.extension.Extensionファイルに追加することで登録できます。

com.example.extensions.LoggingExtension

このメカニズムを有効にするには、junit.jupiter.extensions.autodetection.enabled構成キーをtrueに設定する必要もあります。 これは、–Djunit.jupiter.extensions.autodetection.enabled=trueプロパティを使用してJVMを起動するか、LauncherDiscoveryRequestに構成パラメーターを追加することで実行できます。

LauncherDiscoveryRequest request
  = LauncherDiscoveryRequestBuilder.request()
  .selectors(selectClass("com.example.EmployeesTest"))
  .configurationParameter("junit.jupiter.extensions.autodetection.enabled", "true")
.build();

5.2. プログラムによる拡張登録

アノテーションを使用して拡張機能を登録することは、より宣言的で目立たないアプローチですが、重大な欠点があります:we can’t easily customize the extension behavior。 たとえば、現在の拡張機能登録モデルでは、クライアントからデータベース接続プロパティを受け入れることはできません。

宣言型アノテーションベースのアプローチに加えて、JUnitは拡張機能programmatically. を登録するためのAPIを提供します。たとえば、接続プロパティを受け入れるようにJdbcConnectionUtil classを改造できます。

public class JdbcConnectionUtil {

    private static Connection con;

    // no-arg getConnection

    public static Connection getConnection(String url, String driver, String username, String password) {
        if (con == null) {
            // create connection
            return con;
        }

        return con;
    }
}

また、カスタマイズされたデータベースプロパティをサポートするために、EmployeeDatabaseSetupExtension extensionの新しいコンストラクターを追加する必要があります。

public EmployeeDatabaseSetupExtension(String url, String driver, String username, String password) {
    con = JdbcConnectionUtil.getConnection(url, driver, username, password);
    employeeDao = new EmployeeJdbcDao(con);
}

ここで、従業員拡張機能をカスタムデータベースプロパティに登録するには、静的フィールドに@RegisterExtension annotationアノテーションを付ける必要があります。

@ExtendWith({EnvironmentExtension.class, EmployeeDaoParameterResolver.class})
public class ProgrammaticEmployeesUnitTest {

    private EmployeeJdbcDao employeeDao;

    @RegisterExtension
    static EmployeeDatabaseSetupExtension DB =
      new EmployeeDatabaseSetupExtension("jdbc:h2:mem:AnotherDb;DB_CLOSE_DELAY=-1", "org.h2.Driver", "sa", "");

    // same constrcutor and tests as before
}

ここでは、インメモリH2データベースに接続してテストを実行しています。

5.3. 登録注文

JUnit registers @RegisterExtension static fields after registering extensions that are declaratively defined using the @ExtendsWith annotation.プログラムによる登録に非静的フィールドを使用することもできますが、これらはテストメソッドのインスタンス化とポストプロセッサの後に登録されます。

@RegisterExtensionを介してプログラムで複数の拡張機能を登録する場合、JUnitはそれらの拡張機能を決定論的な順序で登録します。 順序付けは決定的ですが、順序付けに使用されるアルゴリズムは非自明で内部的です。 enforce a particular registration ordering, we can use the @Order annotation:

public class MultipleExtensionsUnitTest {

    @Order(1)
    @RegisterExtension
    static EmployeeDatabaseSetupExtension SECOND_DB = // omitted

    @Order(0)
    @RegisterExtension
    static EmployeeDatabaseSetupExtension FIRST_DB = // omitted

    @RegisterExtension
    static EmployeeDatabaseSetupExtension LAST_DB = // omitted

    // omitted
}

ここで、拡張子はordered based on priority, where a lower value has greater priority than a higher valueです。 また、@Order annotationのない拡張機能は、可能な限り低い優先度になります。

6. 結論

このチュートリアルでは、JUnit 5拡張モデルを使用してカスタムテスト拡張機能を作成する方法を示しました。

例の完全なソースコードはover on GitHubにあります。