1. 概要

今日のアプリケーションは孤立して存在していません。通常、PostgreSQL、Apache Kafka、Cassandra、Redis、その他の外部APIなどのさまざまな外部コンポーネントに接続する必要があります。

このチュートリアルでは、Spring Framework 5.2.5が動的プロパティの導入により、このようなアプリケーションのテストをどのように容易にするかを見ていきます。

まず、問題を定義し、理想的とは言えない方法で問題を解決するためにどのように使用したかを確認することから始めます。 次に、 @DynamicPropertySource アノテーションを紹介し、同じ問題に対してより良いソリューションを提供する方法を確認します。 最後に、純粋なSpringソリューションと比較して優れている可能性があるテストフレームワークの別のソリューションについても見ていきます。

2. 問題:動的プロパティ

データベースとしてPostgreSQLを使用する典型的なアプリケーションを開発しているとしましょう。 簡単なJPAentityから始めましょう。

@Entity
@Table(name = "articles")
public class Article {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    private Long id;

    private String title;

    private String content;

    // getters and setters
}

このエンティティが期待どおりに機能することを確認するには、データベースの相互作用を検証するためのテストを作成する必要があります。 このテストは実際のデータベースと通信する必要があるため、事前にPostgreSQLインスタンスを設定する必要があります。

テスト実行中にそのようなインフラストラクチャツールをセットアップするためのさまざまなアプローチがあります。 実際のところ、このようなソリューションには3つの主要なカテゴリがあります。

  • テスト専用の別のデータベースサーバーをどこかにセットアップします
  • H2などの軽量でテスト固有の代替品または偽物を使用する
  • テスト自体にデータベースのライフサイクルを管理させます

テスト環境と本番環境を区別するべきではないため、H2などのテストダブルを使用するよりも優れた代替手段があります。 3番目のオプションは、実際のデータベースでの作業に加えて、テストの分離を向上させます。 さらに、Dockerや Testcontainers などのテクノロジーを使用すると、3番目のオプションを簡単に実装できます。

Testcontainersなどのテクノロジーを使用した場合のテストワークフローは次のようになります。

  1. すべてのテストの前に、PostgreSQLなどのコンポーネントをセットアップします。 通常、これらのコンポーネントはランダムポートをリッスンします。
  2. テストを実行します。
  3. コンポーネントを分解します。

PostgreSQLコンテナが毎回ランダムなポートをリッスンする場合は、どういうわけか、spring.datasource.url構成プロパティを動的に設定および変更する必要があります。 基本的に、各テストには、その構成プロパティの独自のバージョンが必要です。

構成が静的な場合、Spring Bootの構成管理機能を使用して簡単に管理できます。 ただし、動的な構成に直面している場合、同じタスクが困難になる可能性があります。

問題がわかったので、それに対する従来の解決策を見てみましょう。

3. 従来のソリューション

動的プロパティを実装する最初のアプローチは、カスタムApplicationContextInitializerを使用することです。 基本的に、最初にインフラストラクチャをセットアップし、最初のステップの情報を使用してApplicationContextをカスタマイズします。

@SpringBootTest
@Testcontainers
@ContextConfiguration(initializers = ArticleTraditionalLiveTest.EnvInitializer.class)
class ArticleTraditionalLiveTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:11")
      .withDatabaseName("prop")
      .withUsername("postgres")
      .withPassword("pass")
      .withExposedPorts(5432);

    static class EnvInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

        @Override
        public void initialize(ConfigurableApplicationContext applicationContext) {
            TestPropertyValues.of(
              String.format("spring.datasource.url=jdbc:postgresql://localhost:%d/prop", postgres.getFirstMappedPort()),
              "spring.datasource.username=postgres",
              "spring.datasource.password=pass"
            ).applyTo(applicationContext);
        }
    }

    // omitted 
}

このやや複雑な設定を見ていきましょう。 JUnitは、何よりも先にコンテナーを作成して開始します。 コンテナの準備ができたら、Spring拡張機能は初期化子を呼び出して、動的構成をSpring Environmentに適用します。 明らかに、このアプローチは少し冗長で複雑です。

これらの手順の後でのみ、テストを作成できます。

@Autowired
private ArticleRepository articleRepository;

@Test
void givenAnArticle_whenPersisted_thenShouldBeAbleToReadIt() {
    Article article = new Article();
    article.setTitle("A Guide to @DynamicPropertySource in Spring");
    article.setContent("Today's applications...");

    articleRepository.save(article);

    Article persisted = articleRepository.findAll().get(0);
    assertThat(persisted.getId()).isNotNull();
    assertThat(persisted.getTitle()).isEqualTo("A Guide to @DynamicPropertySource in Spring");
    assertThat(persisted.getContent()).isEqualTo("Today's applications...");
}

4. @DynamicPropertySource

Spring Framework 5.2.5では、動的な値を使用してプロパティを追加しやすくするために@DynamicPropertySourceアノテーションが導入されました @DynamicPropertySource で注釈が付けられ、入力として1つの DynamicPropertyRegistryインスタンスを持つ静的メソッドを作成するだけです。

@SpringBootTest
@Testcontainers
public class ArticleLiveTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:11")
      .withDatabaseName("prop")
      .withUsername("postgres")
      .withPassword("pass")
      .withExposedPorts(5432);

    @DynamicPropertySource
    static void registerPgProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", 
          () -> String.format("jdbc:postgresql://localhost:%d/prop", postgres.getFirstMappedPort()));
        registry.add("spring.datasource.username", () -> "postgres");
        registry.add("spring.datasource.password", () -> "pass");
    }
    
    // tests are same as before
}

上に示したように、私たちは add(String、Supplier)。 与えられた方法 DynamicPropertyRegistry Springにいくつかのプロパティを追加します環境 。 このアプローチは、以前に見たイニシャライザーと比較してはるかにクリーンです。 @DynamicPropertySourceで注釈が付けられたメソッドは、静的として宣言する必要があり、DynamicPropertyRegistryタイプの引数を1つだけ受け入れる必要があることに注意してください。 

基本的に、 @DynmicPropertySource アノテーションの背後にある主な動機は、すでに可能であったことをより簡単に促進することです。 当初はTestcontainersで動作するように設計されていましたが、動的構成で動作する必要がある場合はどこでも使用できます。

5. 代替案:テストフィクスチャ

これまでのところ、どちらのアプローチでも、フィクスチャのセットアップとテストコードは緊密に絡み合っています。 場合によっては、この2つの懸念事項の緊密な結合により、テストコードが複雑になることがあります。特に、設定するものが複数ある場合はそうです。 単一のテストでPostgreSQLとApacheKafkaを使用した場合のインフラストラクチャのセットアップがどのようになるか想像してみてください。

それに加えて、インフラストラクチャのセットアップと動的構成の適用は、それらを必要とするすべてのテストで複製されます

これらの欠点を回避するために、ほとんどのテストフレームワークが提供するテストフィクスチャ機能を使用できます。 たとえば、JUnit 5では、テストクラスのすべてのテストの前にPostgreSQLインスタンスを開始し、Spring Bootを構成し、テストの実行後にPostgreSQLインスタンスを停止するextensionを定義できます。

public class PostgreSQLExtension implements BeforeAllCallback, AfterAllCallback {

    private PostgreSQLContainer<?> postgres;

    @Override
    public void beforeAll(ExtensionContext context) {
        postgres = new PostgreSQLContainer<>("postgres:11")
          .withDatabaseName("prop")
          .withUsername("postgres")
          .withPassword("pass")
          .withExposedPorts(5432);

        postgres.start();
        String jdbcUrl = String.format("jdbc:postgresql://localhost:%d/prop", postgres.getFirstMappedPort());
        System.setProperty("spring.datasource.url", jdbcUrl);
        System.setProperty("spring.datasource.username", "postgres");
        System.setProperty("spring.datasource.password", "pass");
    }

    @Override
    public void afterAll(ExtensionContext context) {
        // do nothing, Testcontainers handles container shutdown
    }
}

ここでは、AfterAllCallbackBeforeAllCallbackを実装して、JUnit5拡張機能を作成しています。 このように、JUnit5はすべてのテストを実行する前にbeforeAll()ロジックを実行し、テストの実行後に afterAll()メソッドのロジックを実行します。 このアプローチでは、テストコードは次のようにクリーンになります。

@SpringBootTest
@ExtendWith(PostgreSQLExtension.class)
@DirtiesContext
public class ArticleTestFixtureLiveTest {
    // just the test code
}

ここでは、@DirtiesContextアノテーションもテストクラスに追加しました。 重要なのは、このがアプリケーションコンテキストを再作成し、テストクラスがランダムポートで実行されている別のPostgreSQLインスタンスと対話できるようにすることです。 結果として、これは、別のデータベースインスタンスに対して、互いに完全に分離してテストを実行します。

読みやすくなるだけでなく、 @ExtendWith(PostgreSQLExtension.class)アノテーションを追加するだけで、同じ機能を簡単に再利用できます。 他の2つのアプローチで行ったように、PostgreSQLセットアップ全体を必要な場所にコピーアンドペーストする必要はありません。

6. 結論

このチュートリアルでは、データベースなどに依存するSpringコンポーネントをテストするのがどれほど難しいかを最初に確認しました。 次に、この問題に対する3つのソリューションを紹介し、それぞれが以前のソリューションが提供していたものを改善しました。

いつものように、すべての例はGitHubから入手できます。