1. 概要

この記事では、JUnit5テストライブラリの拡張モデルを見ていきます。 名前が示すように、 Junit 5拡張の目的は、テストクラスまたはメソッドの動作を拡張することであり、これらは複数のテストに再利用できます。

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

2. JUnit5拡張モデル

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

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

  • テストインスタンスの後処理
  • 条件付きテストの実行
  • ライフサイクルコールバック
  • パラメータ解像度
  • 例外処理

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

3. Mavenの依存関係

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

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.8.1</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 h2 、およびlog4j-coreの最新バージョンはMavenCentralからダウンロードできます。

4. JUnit5拡張機能の作成

JUnit 5拡張機能を作成するには、JUnit5拡張機能ポイントに対応する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インターフェースを実装することによって定義されます。

このインターフェイスを実装し、 EvaluationExecutionCondition()メソッドをオーバーライドする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構成キーを条件に一致するパターンに設定することで、条件を非アクティブ化できます。

これは、JVMを次のコマンドで起動することで実現できます。 -Djunit.conditions.deactivate = プロパティ、または構成パラメータをに追加することによって 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. 前にすべて
  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();
}

次に、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インターフェースを実装し、 supportedParameter()および 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. 拡張機能の登録

テスト拡張機能を定義したので、それらをJUnit5テストに登録する必要があります。 これを実現するために、@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;
    }
}

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

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

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

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

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

5.1. 自動延長登録

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

com.baeldung.extensions.LoggingExtension

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

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

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

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

宣言型アノテーションベースのアプローチに加えて、JUnitは拡張機能を登録するためのAPIを提供します p 文法的に。 たとえば、 JdbcConnectionUtil 接続プロパティを受け入れるクラス:

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拡張機能の新しいコンストラクターを追加する必要があります。

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

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

@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は、@ ExtendsWithアノテーションを使用して宣言的に定義された拡張機能を登録した後、@ RegisterExtension静的フィールドを登録します。プログラムによる登録に非静的フィールドを使用することもできますが、テストメソッドのインスタンス化とポストプロセッサーの後に登録されます。 。

@RegisterExtension を介してプログラムで複数の拡張機能を登録する場合、JUnitはそれらの拡張機能を決定論的な順序で登録します。 順序付けは決定論的ですが、順序付けに使用されるアルゴリズムは自明ではなく、内部的なものです。 特定の登録順序を適用するには、@ Orderアノテーションを使用できます:

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
}

ここで、拡張機能は優先度に基づいて順序付けられます。ここで、低い値は高い値よりも優先度が高くなります。 また、 @Order アノテーションのない拡張機能は、可能な限り低い優先度になります。

6. 結論

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

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