1. 概要

Spring Data JPAは、データベースクエリを作成し、埋め込まれたH2データベースでそれらをテストする簡単な方法を提供します。

ただし、場合によっては、実際のデータベースでのテストの方がはるかに有益です。特にプロバイダー依存のクエリを使用する場合はそうです。

このチュートリアルでは、Testcontainersを使用してSpring DataJPAおよびPostgreSQLデータベースとの統合テストを行う方法を示します。

前のチュートリアルでは、主に@Queryアノテーションを使用してデータベースクエリを作成しました。これをテストします。

2. 構成

テストでPostgreSQLデータベースを使用するには、テストスコープPostgreSQLドライバーを使用したTestcontainers依存関係をpom.xmlに追加する必要があります。

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <version>1.10.6</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.2.5</version>
</dependency>

また、テストリソースディレクトリの下に application.properties ファイルを作成します。このファイルでは、Springに適切なドライバークラスを使用し、テストの実行ごとにスキームを作成して削除するように指示します。

spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=create-drop

3. 単一のテストの使用

単一のテストクラスでPostgreSQLインスタンスの使用を開始するには、最初にコンテナ定義を作成してから、そのパラメータを使用して接続を確立する必要があります。

@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(initializers = {UserRepositoryTCIntegrationTest.Initializer.class})
public class UserRepositoryTCIntegrationTest extends UserRepositoryCommonIntegrationTests {

    @ClassRule
    public static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer("postgres:11.1")
      .withDatabaseName("integration-tests-db")
      .withUsername("sa")
      .withPassword("sa");

    static class Initializer
      implements ApplicationContextInitializer<ConfigurableApplicationContext> {
        public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
            TestPropertyValues.of(
              "spring.datasource.url=" + postgreSQLContainer.getJdbcUrl(),
              "spring.datasource.username=" + postgreSQLContainer.getUsername(),
              "spring.datasource.password=" + postgreSQLContainer.getPassword()
            ).applyTo(configurableApplicationContext.getEnvironment());
        }
    }
}

上記の例では、JUnitの @ClassRule を使用して、テストメソッドを実行する前にデータベースコンテナーをセットアップしました。 また、 ApplicationContextInitializerを実装する静的内部クラスを作成しました。最後のステップとして、 @ContextConfiguration アノテーションを、初期化クラスをパラメーターとしてテストクラスに適用しました。

これらの3つのアクションを実行することにより、Springコンテキストが公開される前に接続プロパティを設定できます。

前の記事の2つのUPDATEクエリを使用してみましょう。

@Modifying
@Query("update User u set u.status = :status where u.name = :name")
int updateUserSetStatusForName(@Param("status") Integer status, 
  @Param("name") String name);

@Modifying
@Query(value = "UPDATE Users u SET u.status = ? WHERE u.name = ?", 
  nativeQuery = true)
int updateUserSetStatusForNameNative(Integer status, String name);

そして、構成された環境でそれらをテストします。

@Test
@Transactional
public void givenUsersInDB_WhenUpdateStatusForNameModifyingQueryAnnotationJPQL_ThenModifyMatchingUsers(){
    insertUsers();
    int updatedUsersSize = userRepository.updateUserSetStatusForName(0, "SAMPLE");
    assertThat(updatedUsersSize).isEqualTo(2);
}

@Test
@Transactional
public void givenUsersInDB_WhenUpdateStatusForNameModifyingQueryAnnotationNative_ThenModifyMatchingUsers(){
    insertUsers();
    int updatedUsersSize = userRepository.updateUserSetStatusForNameNative(0, "SAMPLE");
    assertThat(updatedUsersSize).isEqualTo(2);
}

private void insertUsers() {
    userRepository.save(new User("SAMPLE", "[email protected]", 1));
    userRepository.save(new User("SAMPLE1", "[email protected]", 1));
    userRepository.save(new User("SAMPLE", "[email protected]", 1));
    userRepository.save(new User("SAMPLE3", "[email protected]", 1));
    userRepository.flush();
}

上記のシナリオでは、最初のテストは成功して終了しますが、2番目のテストはInvalidDataAccessResourceUsageExceptionを次のメッセージとともにスローします。

Caused by: org.postgresql.util.PSQLException: ERROR: column "u" of relation "users" does not exist

H2組み込みデータベースを使用して同じテストを実行すると、両方のテストは正常に完了しますが、PostgreSQLはSET句でエイリアスを受け入れません。 問題のあるエイリアスを削除することで、クエリをすばやく修正できます。

@Modifying
@Query(value = "UPDATE Users u SET status = ? WHERE u.name = ?", 
  nativeQuery = true)
int updateUserSetStatusForNameNative(Integer status, String name);

今回は、両方のテストが正常に完了します。 この例では、 Testcontainersを使用して、本番環境で実際のデータベースに切り替えた後に明らかになるネイティブクエリの問題を特定しました。 JPQLクエリを使用するとSpringは、使用するデータベースプロバイダーに応じて適切に変換するため、一般的に安全です。

3.1. 構成を使用したテストごとに1つのデータベース

これまで、テストクラス内ですべてのテストを実行する前に、JUnit4ルールを使用してデータベースインスタンスを起動してきました。 最終的に、このアプローチでは、各テストクラスの前にデータベースインスタンスを作成し、各クラスですべてのテストを実行した後にデータベースインスタンスを破棄します。

このアプローチは、テストインスタンス間の最大の分離を作成します。 また、データベースを複数回起動するオーバーヘッドにより、テストが遅くなる可能性があります。

JUnit 4ルールアプローチに加えて、JDBC URLを変更し、テストクラスごとにデータベースインスタンスを作成するようにTestcontainersに指示できます。 このアプローチは、テストでインフラストラクチャコードを記述する必要なしに機能します。

たとえば、上記の例を書き直すには、これをapplication.propertiesに追加するだけです。

spring.datasource.url=jdbc:tc:postgresql:11.1:///integration-tests-db

“ tc:” は、コードを変更せずにTestcontainersがデータベースインスタンスをインスタンス化するようにします。 したがって、テストクラスは次のように単純になります。

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class UserRepositoryTCJdbcLiveTest extends UserRepositoryCommon {

    @Test
    @Transactional
    public void givenUsersInDB_WhenUpdateStatusForNameModifyingQueryAnnotationNative_ThenModifyMatchingUsers() {
        // same as above
    }
}

テストクラスごとに1つのデータベースインスタンスを作成する場合は、このアプローチが推奨されます。

4. 共有データベースインスタンス

前の段落では、単一のテストでTestcontainersを使用する方法について説明しました。 実際のシナリオでは、起動時間が比較的長いため、同じデータベースコンテナを複数のテストで再利用したいと考えています。

次に、 PostgreSQLContainer を拡張し、 start()メソッドと stop()メソッドをオーバーライドして、データベースコンテナー作成用の共通クラスを作成しましょう。

public class BaeldungPostgresqlContainer extends PostgreSQLContainer<BaeldungPostgresqlContainer> {
    private static final String IMAGE_VERSION = "postgres:11.1";
    private static BaeldungPostgresqlContainer container;

    private BaeldungPostgresqlContainer() {
        super(IMAGE_VERSION);
    }

    public static BaeldungPostgresqlContainer getInstance() {
        if (container == null) {
            container = new BaeldungPostgresqlContainer();
        }
        return container;
    }

    @Override
    public void start() {
        super.start();
        System.setProperty("DB_URL", container.getJdbcUrl());
        System.setProperty("DB_USERNAME", container.getUsername());
        System.setProperty("DB_PASSWORD", container.getPassword());
    }

    @Override
    public void stop() {
        //do nothing, JVM handles shut down
    }
}

stop()メソッドを空のままにしておくことで、JVMがコンテナーのシャットダウンを処理できるようになります。 また、最初のテストのみがコンテナーの起動をトリガーし、後続の各テストが既存のインスタンスを使用する、単純なシングルトンパターンを実装します。 start()メソッドでは、 System#setProperty を使用して、接続パラメーターを環境変数として設定します。

これで、それらをapplication.propertiesファイルに入れることができます。

spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}

テスト定義でユーティリティクラスを使用してみましょう。

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserRepositoryTCAutoIntegrationTest {

    @ClassRule
    public static PostgreSQLContainer postgreSQLContainer = BaeldungPostgresqlContainer.getInstance();

    // tests
}

前の例と同様に、コンテナ定義を保持するフィールドに@ClassRuleアノテーションを適用しました。 このようにして、Springコンテキストを作成する前に、DataSource接続プロパティに正しい値が入力されます。

BaeldungPostgresqlContainerユーティリティクラスでインスタンス化された@ClassRule注釈付きフィールドを定義するだけで、同じデータベースインスタンスを使用して複数のテストを実装できるようになりました。

5. 結論

この記事では、Testcontainersを使用して実際のデータベースインスタンスでテストを実行する方法を説明しました。

SpringのApplicationContextInitializerメカニズムを使用し、再利用可能なデータベースのインスタンス化のためのクラスを実装して、単一のテストの使用例を確認しました。

また、Testcontainersが、特にネイティブクエリの場合に、複数のデータベースプロバイダー間の互換性の問題を特定するのにどのように役立つかを示しました。

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