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

1概要

この記事では、JUnit 5テスト・ライブラリーの拡張モデルを見ていきます。その名前が示すように、** Junit 5の拡張機能の目的はテストクラスやメソッドの振る舞いを拡張することであり、これらは複数のテストに再利用できます。

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

2 JUnit 5拡張モデル

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

主に5種類の拡張ポイントを使用できます。

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

  • 条件付きテスト実行

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

  • パラメータ解決

  • 例外処理

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

3 Mavenの依存関係

まず、例に必要なプロジェクトの依存関係を追加しましょう。

私たちが必要とする主なJUnit 5ライブラリは junit-jupiter-engine です。

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.1.0</version>
    <scope>test</scope>
</dependency>

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

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.8.2</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.196</version>
</dependency>

junit-jupiter-engine 、https://search.maven.org/classic/#searchの最新バージョン%7Cga%7C1%7Ca%3A%22h2%22%20AND%20g%3A%22com.h2database%22[h2]およびhttps://search.maven.org/classic/#search%7Cga%7C1%7Ca%3A %22log4j-core%22%20AND%20g%3A%22org.apache.logging.log4j%22[log4j-core]はMaven Centralからダウンロードできます。

4 JUnit 5の拡張機能を作成する

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

4.1. TestInstancePostProcessor Extension

この種の拡張はテストのインスタンスが作成された後に実行されます。実装するインターフェースは 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” 環境では実行されません。

  • 条件を検証したくない場合は、 junit.conditions.deactivate 設定キーを** 条件に一致するパターンに設定して無効にすることができます。

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

public class TestLauncher {
    public static void main(String[]args) {
        LauncherDiscoveryRequest request
          = LauncherDiscoveryRequestBuilder.request()
          .selectors(selectClass("com.baeldung.EmployeesTest"))
          .configurationParameter(
            "junit.conditions.deactivate",
            "com.baeldung.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. テスト

  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<Employee> 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();
}

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

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” とは異なる環境でのみ実行されます。

このテストでは、 EmployeeDatabaseSetupExtension を追加して、 employees テーブルを作成し、各メソッドをトランザクションにラップします。

whenAddEmployee thenGetEmploee()__テストが最初に実行されてテーブルにレコードが1つ追加された場合でも、2番目のテストではテーブル内に0レコードが見つかります。

Loggerインスタンスは、 LoggingExtension を使用してクラスに追加されます。

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

5.1. 自動内線登録

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

com.baeldung.extensions.LoggingExtension

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

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

6. 結論

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

例の完全なソースコードはhttps://github.com/eugenp/tutorials/tree/master/testing-modules/junit-5[GitHubに乗って]を参照してください。