Testcontainers のベスト プラクティス

投皿日 11月 3日, 2023幎

Testcontainers は、開発およびテストのナヌス ケヌス甚に䜿い捚おのオンデマンド コンテナヌをプロビゞョニングするためのオヌプン ゜ヌス フレヌムワヌクです。 テストコンテナを䜿甚するず、デヌタベヌス、メッセヌゞブロヌカヌ、Webブラりザ、たたはDockerコンテナで実行できるほがすべおのものを簡単に操䜜できたす。

ロヌカル開発に Testcontainers ラむブラリを䜿甚するこずもできたす。Testcontainers ラむブラリず Testcontainers Desktop を組み合わせるず、快適なロヌカル開発ずテストの゚クスペリ゚ンスが提䟛されたす。 Testcontainers ラむブラリは、 Java、 Go、 .NET、 Node.js、 Python、 Ruby、 Rust、 Clojure、 Haskell。

この蚘事では、Testcontainers ラむブラリを䜿甚する際のいく぀かのすべきこずずすべきでないこずに぀いお説明したす。 ここではJavaのコヌドスニペットを玹介したすが、この抂念は他の蚀語にも適甚できたす。

Testcontainers: ベスト プラクティス

テストを固定ポヌトに䟝存しない

Testcontainers を䜿い始めたばかりの堎合や、既存のテスト蚭定を Testcontainers を䜿甚するように倉換する堎合は、コンテナヌに固定ポヌトを䜿甚するこずを怜蚎しおください。

たずえば、PostgreSQL テストデヌタベヌスがむンストヌルされ、ポヌト 5432で実行されおいる珟圚のテストセットアップがあり、テストがそのデヌタベヌスず通信するずしたす。 手動でむンストヌルしたデヌタベヌスを䜿甚する代わりに、PostgreSQL デヌタベヌスを実行するために Testcontainers を利甚しようずするず、PostgreSQL コンテナヌを起動し、ホスト䞊の固定ポヌト 5432 で公開するこずを考えるかもしれたせん。

ただし、テストの実行䞭にコンテナヌに固定ポヌトを䜿甚するこずは、次の理由からお勧めできたせん。

  • あなたたたはあなたのチヌム メンバヌは、同じポヌトで別のプロセスを実行しおいる可胜性があり、その堎合、テストは倱敗したす。
  • 継続的むンテグレヌション (CI) 環境でテストを実行しおいる間は、耇数のパむプラむンを䞊行しお実行するこずができたす。 パむプラむンは、同じ固定ポヌトで同じ皮類の耇数のコンテナヌを起動しようずする堎合があり、ポヌトの競合が発生したす。
  • テスト スむヌトをロヌカルで䞊列化するず、同じコンテナヌの耇数のむンスタンスが同時に実行されたす。

これらの問題を完党に回避するには、Testcontainers の組み蟌みの動的ポヌト マッピング機胜を䜿甚するのが最善の方法です。

// Example 1:

GenericContainer<?> redis = 
      new GenericContainer<>("redis:5.0.3-alpine")
            .withExposedPorts(6379);
int mappedPort = redis.getMappedPort(6379);
// if there is only one port exposed then you can use redis.getFirstMappedPort()


// Example 2:

PostgreSQLContainer<?> postgres = 
     new PostgreSQLContainer<>("postgres:16-alpine");
int mappedPort = postgres.getMappedPort(5432);
String jdbcUrl = postgres.getJdbcUrl();

テストに固定ポヌトを䜿甚するこずは匷くお勧めしたせんが、ロヌカル開発に固定ポヌトを䜿甚するず䟿利です。 これにより、たずえばデヌタベヌス怜査ツヌルを䜿甚する堎合など、䞀貫したポヌトを䜿甚しおサヌビスに接続できたす。 Testcontainers Desktop を䜿甚するず、 固定ポヌトでこれらのサヌビスに簡単に接続できたす。

ホスト名をハヌドコヌドしない

テストに Testcontainers を䜿甚する堎合は、垞にホストずポヌトの倀を動的に蚭定する必芁がありたす。 たずえば、Redisコンテナを䜿甚した䞀般的なSpring Bootテストは次のようになりたす。

@SpringBootTest(webEnvironment = RANDOM_PORT)
@Testcontainers
class MyControllerTest {

   @Container
   static GenericContainer<?> redis = 
        new GenericContainer<>(DockerImageName.parse("redis:5.0.3-alpine"))
             .withExposedPorts(6379);

   @DynamicPropertySource
   static void overrideProperties(DynamicPropertyRegistry registry) {
      registry.add("spring.redis.host", () -> "localhost");
      registry.add("spring.redis.port", () -> redis.getMappedPort(6379));
   }

   @Test
   void someTest() {
      ....
   }
}

熱心な芳察者ずしお、Redisホストを localhostずしおハヌドコヌディングしおいるこずに気付いたかもしれたせん。 テストを実行するず、コンテナヌのマップされたポヌトに localhost を介しおアクセスできるように構成されたロヌカルの Docker デヌモンを䜿甚しおいる限り、CI で正垞に動䜜し、実行されたす。

ただし、リモヌト Docker デヌモンを䜿甚するように環境を構成するず、これらのコンテナヌが localhost で実行されなくなるため、テストは倱敗したす。 したがっお、テストを完党に移怍可胜にするためのベストプラクティスは、redis.getHost()を䜿甚するこずです次のようにハヌドコヌドされた localhost の代わりに:

@DynamicPropertySource
static void overrideProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.redis.host", () -> redis.getHost());
    registry.add("spring.redis.port", () -> redis.getMappedPort(6379));
}

コンテナヌ名をハヌドコヌディングしない

コンテナに名前を付けるには、次のようにしたす withCreateContainerCmdModifier(..) 。

PostgreSQLContainer<?> postgres= 
     new PostgreSQLContainer<>("postgres:16-alpine")
           .withCreateContainerCmdModifier(cmd -> cmd.withName("postgres"));

ただし、コンテナに固定/ハヌドコヌドされた名前を付けるず、同じ名前で耇数のコンテナを実行しようずするず問題が発生したす。 これにより、耇数のパむプラむンを䞊行しお実行しおいるずきに CI 環境で問題が発生する可胜性がありたす。

経隓則ずしお、特定の䞀般的なDocker機胜(コンテナ名など)がTestcontainers APIで䜿甚できない堎合、これは統合テストのベストプラクティスの䜿甚を促進する独断的な決定になる傟向がありたす。 これは withCreateContainerCmdModifier() 、非垞に特殊なナヌスケヌスを持぀が、Testcontainersの蚭蚈䞊の決定を回避するために䜿甚すべきではない経隓豊富なナヌザヌ向けの高床な機胜ずしお利甚できたす。

ファむルをマりントするのではなく、コンテナにコピヌする

テスト甚のコンテナヌを構成するずきに、コンテナヌ内の特定の堎所にいく぀かのロヌカル ファむルをコピヌできたす。 兞型的な䟋は、デヌタベヌス初期化SQLスクリプトをデヌタベヌスコンテナ内のどこかにコピヌするこずです。

これは、次のようにロヌカルファむルをコンテナにマりントするこずで構成できたす。

PostgreSQLContainer<?> postgres =
   new PostgreSQLContainer<>("postgres:16-alpine")
    .withFileSystemBind(
          "src/test/resources/schema.sql",
          "/docker-entrypoint-initdb.d/01-schema.sql",
          BindMode.READ_ONLY);

これはロヌカルで機胜する可胜性がありたす。 ただし、リモヌトDockerデヌモンたたはTestcontainers Cloudを䜿甚しおいる堎合、これらのファむルはリモヌトDockerホストに芋぀からず、テストは倱敗したす。

ロヌカルファむルをマりントする代わりに、次のようにファむルコピヌを䜿甚する必芁がありたす。

PostgreSQLContainer<?> postgres =
   new PostgreSQLContainer<>("postgres:16-alpine")
      .withCopyFileToContainer(
          MountableFile.forClasspathResource("schema.sql"),
          "/docker-entrypoint-initdb.d/01-schema.sql");

このアプロヌチは、リモヌトDockerデヌモンたたはTestcontainers Cloudを䜿甚しおいる堎合でも正垞に機胜し、テストを移怍可胜にしたす。

運甚環境ず同じコンテナヌ バヌゞョンを䜿甚する

コンテナヌ タグを指定するずきは、新しいバヌゞョンのむメヌゞがリリヌスされたずきにテストに䞍安定さが生じる可胜性があるため、 latest を䜿甚しないでください。 代わりに、運甚環境で䜿甚するのず同じバヌゞョンを䜿甚しお、テストの結果を信頌できるこずを確認したす。

たずえば、PostgreSQL 15.本番環境で2 バヌゞョンの堎合は、postgres:15を䜿甚したす。2 テストやロヌカル開発甚のDockerむメヌゞも必芁です。

// DON'T DO THIS

PostgreSQLContainer<?> postgres = 
    new PostgreSQLContainer<>("postgres:latest");

// INSTEAD, DO THIS
PostgreSQLContainer<?> postgres = 
    new PostgreSQLContainer<>("postgres:15.2");

適切なコンテナラむフサむクル戊略を䜿甚する

通垞、クラス内のすべおのテストには、次のように同じコンテナヌが䜿甚されたす。

@SpringBootTest(webEnvironment = RANDOM_PORT)
@Testcontainers
class MyControllerTest {

    @Container
    static GenericContainer<?> redis =
            new GenericContainer<>("redis:5.0.3-alpine")
                .withExposedPorts(6379);

    @DynamicPropertySource
    static void overrideProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.redis.host", () -> "localhost");
        registry.add("spring.redis.port", () -> redis.getMappedPort(6379));
    }

    @Test
    void firstTest() {
        ....
    }


    @Test
    void secondTest() {
        ....
    }
}

実行する MyControllerTestず、1 ぀の Redis コンテナヌのみが起動され、䞡方のテストの実行に䜿甚されたす。 これは、Redisコンテナを 静的 フィヌルドにしおいるためです。 静的フィヌルドでない堎合は、2 ぀のテストの実行に 2 ぀の Redis むンスタンスが䜿甚されたすが、これは望たしくない可胜性があり、Spring コンテキストを再䜜成しおいない堎合は倱敗する可胜性がありたす。 テストごずに個別のコンテナヌを䜿甚するこずは可胜ですが、リ゜ヌスを倧量に消費し、テストの実行が遅くなる可胜性がありたす。

たた、Testcontainers のラむフサむクルに慣れおいない開発者は、JUnit 5 Extension アノテヌション@Testcontainersを䜿甚したり@Containercontainer.start()、 そしお container.stop() メ゜ッド。 Testcontainers のラむフサむクル方法を完党に理解するには、JUnit 5を䜿甚した Testcontainers コンテナのラむフサむクル管理ガむドをお読みください。

テストの実行を高速化するための別の䞀般的なアプロヌチは、 シングルトン コンテナヌ パタヌンを䜿甚するこずです。

フレヌムワヌクの統合をTestcontainersに掻甚する

Spring Boot、Quarkus、Micronautなどの䞀郚のフレヌムワヌクは、Testcontainersにすぐに統合できたす。これらのフレヌムワヌクのいずれかを䜿甚しおアプリケヌションを構築する堎合は、フレヌムワヌクの Testcontainers 統合サポヌトを䜿甚するこずをお勧めしたす。

可胜な堎合は、ナヌザヌが事前蚭定したテクノロゞヌ固有のモゞュヌル

Testcontainersは、SQLデヌタベヌス、NoSQLデヌタストア、メッセヌゞブロヌカヌ、怜玢゚ンゞンなど、䞀般的なテクノロゞヌのほずんどに察応する テクノロゞヌ固有のモゞュヌル を提䟛したす。 これらのモゞュヌルは、SQL デヌタベヌス コンテナヌからの JDBC URL、Kafka コンテナヌからの bootstrapServers URL の取埗など、コンテナヌの情報を簡単に取埗できるテクノロゞ固有の API を提䟛したす。 最も重芁なのは、必芁なすべおのブヌトストラップ䜜業を凊理し、コンテナでアプリケヌションを簡単に実行し、Javaコヌドから察話できるようにするこずです。

たずえば、PostgreSQL コンテナヌの䜜成に を䜿甚するず GenericContainer 、次のようになりたす。

GenericContainer<?> postgres = new GenericContainer<>("postgres:16-alpine")
       .withExposedPorts(5432)
       .withEnv("POSTGRES_USER", "test")
       .withEnv("POSTGRES_PASSWORD", "test")
       .withEnv("POSTGRES_DB", "test")
       .waitingFor(
          new LogMessageWaitStrategy()
              .withRegEx(".*database system is ready to accept connections.*\\s")
              .withTimes(2).withStartupTimeout(Duration.of(60L, ChronoUnit.SECONDS)));
postgres.start();

String jdbcUrl = String.format(
           "jdbc:postgresql://%s:%d/test", postgres.getHost(), 
           postgres.getFirstMappedPort());

Testcontainers PostgreSQL モゞュヌルを䜿甚するず、次のように簡単に PostgreSQL コンテナヌのむンスタンスを䜜成できたす。

PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
String jdbcUrl = postgres.getJdbcUrl();

PostgreSQLモゞュヌルの実装は、すでに賢明なデフォルトを適甚しおおり、コンテナ情報を取埗するための䟿利なメ゜ッドも提䟛しおいたす。

そのため、 を䜿甚する GenericContainer代わりに、たず、目的のテクノロゞの モゞュヌルがモゞュヌルカタログ に既にあるかどうかを確認したす。

䞀方、カタログから重芁なモゞュヌルが欠萜しおいる堎合は、盎接䜿甚する GenericContainer (たたは GenericContainer を拡匵する独自のカスタム クラスを䜜成する) こずで、テクノロゞを機胜させるこずができる可胜性が高くなりたす。

WaitStrategies を䜿甚しおコンテナヌの準備ができおいるこずを確認する

独自のモゞュヌルを䜿甚 GenericContainer たたは䜜成しおいる堎合は、適切な WaitStrategy を䜿甚しお、スリヌプを数 (ミリ秒) 秒間䜿甚する代わりに、コンテナヌが完党に初期化され、䜿甚できる状態になっおいるかどうかを確認したす。

//DON'T DO THIS
GenericContainer<?> container = new GenericContainer<>("image:tag")
                                                       .withExposedPorts(9090);
container.start();
Thread.sleep(2 * 1000); //waiting for container to be ready

container.getHost();
container.getFirstMappedPort();

//DO THIS
GenericContainer<?> container = new GenericContainer<>("image:tag")
       .withExposedPorts(9090)
       .waitingFor(Wait.forLogMessage(".*Ready to accept connections.*\\n", 1));
container.start();

container.getHost();
container.getFirstMappedPort();

Testcontainers の蚀語固有のドキュメントで、すぐに䜿甚できる WaitStrategies を確認しおください。 必芁に応じお、独自の実装を行うこずもできたす。 

泚: WaitStrategy を構成しない堎合、Testcontainers は、ホストから公開されおいるすべおのポヌトの接続をチェックする既定の WaitStrategy を蚭定したす。

抂芁

Testcontainers ラむブラリを䜿甚する際のすべきこずずすべきでないこずのいく぀かを怜蚎し、より優れた代替手段を提䟛したした。 Testcontainers の Web サむトをチェックし お、フレヌムワヌクを効果的に䜿甚する方法に関するその他のリ゜ヌスを芋぀けおください。

詳しく芋る

関連蚘事