Testcontainers: 実際の依存関係を使用したテスト

ソフトウェアは時間の経過とともに進化し、自動テストは継続的インテグレーションと継続的デリバリーに不可欠な前提条件です。 開発者は、単体テスト、統合テスト、パフォーマンス テスト、ソフトウェアのさまざまな側面を測定するための E2E テストなど、さまざまな種類のテストを作成します。

通常、単体テストはビジネスロジックのみを検証するために行われます。 また、テスト対象のシステムの部分によっては、外部依存関係がモックされたり、スタブされたりする傾向があります。

しかし、実際のエンドツーエンドの機能はさまざまな外部サービス統合に依存するため、単体テストだけではあまり信頼できません。 そのため、統合テストは、実際の依存関係を使用してシステムの全体的な動作を検証するために使用されます。

従来、統合テストは複雑なプロセスであり、次のものが含まれます。

  • データベース、メッセージブローカーなどの必要な依存サービスのインストールと構成。
  • Web サーバーまたはアプリケーション・サーバーのセットアップ
  • サーバー上でのアーティファクト(jar、war、ネイティブ実行可能ファイルなど)のビルドとデプロイ
  • 統合テストの実行

Testcontainers を使用すると、単体テストの軽量なエクスペリエンスとシンプルさを、実際の依存関係に対して実行される統合テストの信頼性と組み合わせることができます。

実際の依存関係を使用したテストコンテナのテスト

実際の依存関係を使用したテストが重要なのはなぜですか?

テストでは、開発者が実際の開発作業中に迅速なフィードバック サイクルを使用してアプリケーションの動作を検証できるようにする必要があります。

モックやインメモリサービスでテストすると、システムが正常に動作しているという誤った印象を与えるだけでなく、フィードバックサイクルを大幅に遅らせる可能性があります。 実際の依存関係を使用したテストでは、実際のコードが実行され、信頼性が高まります。

運用環境で Postgres または SQL Server を使用しながら、テストに H2 などのインメモリ データベースを使用する一般的なシナリオを考えてみましょう。 これが悪い習慣である理由はいくつかあります。

1. 互換性の問題

重要なアプリケーションでは、インメモリ データベースではサポートされない可能性のあるデータベース固有の機能の一部を利用します。 たとえば、ページネーションを適用する一般的な方法は、 LIMITOFFSETを使用することです。

SELECT id, name FROM employee ORDER BY name LIMIT 25 OFFSET 50

H2 データベースをテストに使用し、MS SQL Server を運用環境に使用することを想像してみてください。 H2でテストすると、テストは成功し、コードが正常に動作しているという誤った印象を与えます。 しかし、MS SQL ServerはLIMITをサポートしていないため、本番環境では失敗します ...OFFSET 構文を使用します。

2. インメモリ データベースは、運用データベースのすべての機能をサポートしているわけではありません

アプリケーションでは、インメモリ データベースで完全にサポートされていないデータベース ベンダー固有の高度な機能が使用される場合があります。 例としては、 XML/JSON 変換関数WINDOW 関数共通テーブル式 (CTE) などがあります。 このような場合、インメモリ データベースを使用してテストすることは不可能です。

これらは、独自のコードでサービスをモックしているときに、さらに大きな問題に発展することがよくあります。 モックは、サービスのコントラクトとして使用するモック定義を正常に抽出できるテスト シナリオに役立ちますが、この互換性の検証は、多くの場合、テスト セットアップを複雑にするだけです。

モックの一般的な使用では、システムの動作が本番環境で機能するかどうかを確実に検証することはできません。 また、コードの非互換性やサードパーティの統合によって引き起こされる問題をキャッチするテストスイートの能力にも自信が持てません。 

そのため、可能な限り実際の依存関係を使用してテストを作成し、必要な場合にのみモックを使用することを強くお勧めします。

Testcontainers を使用した実際の依存関係でのテスト

Testcontainersは、使い捨てのDockerコンテナで実際の依存関係を使用してテストを記述できるようにするテストライブラリです。 これは、必要な依存サービスをDockerコンテナとしてスピンアップするためのプログラム可能なAPIを提供します。 このようにして、モックの代わりに実際のサービスを使用してテストを書くことができます。 そのため、単体テスト、API テスト、エンド ツー エンド テストのいずれを記述しているかに関係なく、同じプログラミング モデルで実際の依存関係を使用してテストを記述できます。

テストコンテナーの図

Testcontainers ライブラリは、次の言語で使用でき、ほとんどのフレームワークおよびテスト ライブラリとうまく統合されています。

  • ジャワ
  • 行く
  • ノード.js 
  • 。網
  • ニシキヘビ

ケーススタディー

Testcontainerを使用してアプリケーションのさまざまなスライスをテストする方法と、それらすべてが「実際の依存関係を持つ単体テスト」にどのように見えるかを見てみましょう。

ここでは、Web アプリを介して利用され、Postgres を使用してデータを格納する 一般的な API サービスを実装する SpringBoot アプリケーションの サンプル コードを使用します。 しかし、Testcontainersはお気に入りの言語の慣用的なAPIを提供しているため、すべての言語で同様のセットアップを実現できます。

これらの例をイラストとして扱い、何ができるかを感じてください。 また、Javaエコシステムに参加している場合は、過去に作成したテストを認識したり、それをどのように行うことができるかについてインスピレーションを得たりします。

データリポジトリのテスト

1つのカスタムメソッドを持つ次のSpring Data JPAリポジトリがあるとします。

public interface TodoRepository extends PagingAndSortingRepository<Todo, String> {
   @Query("select t from Todo t where t.completed is false")
   Iterable<Todo> getPendingTodos();
}

前述したように、テストにインメモリ データベースを使用し、運用環境に別の種類のデータベースを使用することは推奨されず、問題が発生する可能性があります。 運用データベースの種類でサポートされている機能またはクエリ構文が、メモリ内データベースでサポートされていない場合があります。

たとえば、次のクエリ(データ移行スクリプトにある場合があります)は、Postgresqlでは正常に機能しますが、H2の場合は機能しなくなります。

INSERT INTO todos (id, title)
VALUES ('1', 'Learn Modern Integration Testing with Testcontainers')
ON CONFLICT do nothing;

そのため、運用環境で使用されるのと同じ種類のデータベースでテストすることを常にお勧めします。

SpringBoot のスライス テスト アノテーション @DataJpaTestを使用して、TodoRepository の単体テストを記述できます。これを行うには、次のように Testcontainers を使用して Postgres コンテナーをプロビジョニングします。

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class TodoRepositoryTest {
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:14-alpine");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    TodoRepository repository;

    @BeforeEach
    void setUp() {
        repository.deleteAll();
        repository.save(new Todo(null, "Todo Item 1", true, 1));
        repository.save(new Todo(null, "Todo Item 2", false, 2));
        repository.save(new Todo(null, "Todo Item 3", false, 3));
    }

    @Test
    void shouldGetPendingTodos() {
        assertThat(repository.getPendingTodos()).hasSize(2);
    }
}

Postgres データベースの依存関係は、Testcontainers JUnit5 拡張機能を使用してプロビジョニングされ、テストは実際の Postgres データベースと通信します。 コンテナライフサイクル管理の使用の詳細については、「 Testcontainers と JUnit の統合」を参照してください。

インメモリ データベースを使用する代わりに、運用環境で使用されているのと同じ種類のデータベースでテストすることで、データベースの互換性の問題が発生する可能性が完全に回避され、テストの信頼性が高まります。

データベーステストのために、TestcontainersはSQLデータベースでの作業を容易にする 特別なJDBC URLサポート を提供します。

REST API エンドポイントのテスト

APIエンドポイントをテストするには、Testcontainersを介してプロビジョニングされたデータベースなど、必要な依存関係とともにアプリケーションをブートストラップします。 REST API エンドポイントをテストするためのプログラミング・モデルは、リポジトリー単体テストと同じです。

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
public class TodoControllerTests {
    @LocalServerPort
    private Integer port;
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:14-alpine");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    TodoRepository todoRepository;

    @BeforeEach
    void setUp() {
        todoRepository.deleteAll();
        RestAssured.baseURI = "http://localhost:" + port;
    }

    @Test
    void shouldGetAllTodos() {
        List<Todo> todos = List.of(
                new Todo(null, "Todo Item 1", false, 1),
                new Todo(null, "Todo Item 2", false, 2)
        );
        todoRepository.saveAll(todos);

        given()
                .contentType(ContentType.JSON)
                .when()
                .get("/todos")
                .then()
                .statusCode(200)
                .body(".", hasSize(2));
    }
}

@SpringBootTest アノテーションを使用してアプリケーションをブートストラップし API 呼び出しと応答の検証に RestAssured を使用しました。 これにより、モックが関与しないため、テストの信頼性が高まり、開発者はAPIとの接触を中断することなく、あらゆる種類の内部コードリファクタリングを行うことができます。

SeleniumとTestcontainersを使用したエンドツーエンドのテスト

Seleniumは、エンドツーエンドのテストを実行するための一般的なブラウザ自動化ツールです。 Testcontainersは、Dockerコンテナでのセレンベースのテストの実行を簡素化するSeleniumモジュールを提供します。

@Testcontainers
public class SeleniumE2ETests {
   @Container
   static BrowserWebDriverContainer<?> chrome = new BrowserWebDriverContainer<>().withCapabilities(new ChromeOptions());
 
   static RemoteWebDriver driver;
   
   @BeforeAll
   static void beforeAll() {
       driver = new RemoteWebDriver(chrome.getSeleniumAddress(), new ChromeOptions());
   }
 
   @AfterAll
   static void afterAll() {
       driver.quit();
   }
 
   @Test
   void testViewHomePage() {
      String baseUrl = "https://myapp.com";
      driver.get(baseUrl);
      assertThat(driver.getTitle()).isEqualTo("App Title");
   }
}

Testcontainers が提供する WebDriver と同じプログラミング モデルを使用して Selenium テストを実行できます。 Testcontainersを使用すると、複雑な構成設定を行うことなく、テスト実行のビデオを簡単に録画できます。

Testcontainers Java SpringBoot QuickStart プロジェクトを参照して参照できます。

結論

開発者がアプリケーションに使用するさまざまなタイプのテスト(データアクセスレイヤー、APIテスト、さらにはエンドツーエンドテスト)を調べました。 また、Testcontainers ライブラリを使用すると、運用環境で使用するデータベースの実際のバージョンなど、実際の依存関係でこれらを実行するためのセットアップが簡素化される方法もわかりました。 

Testcontainers は、Java、Go、.NET、Python など、複数の一般的なプログラミング言語で利用できます。 また、実際の依存関係を持つテストを、開発者が使い慣れた単体テストに変換するための慣用的なアプローチも提供します。

テストコンテナベースのテストは、IDEを介して個々のテストを実行するか、テストのクラスを実行するか、コマンドラインからスイート全体を実行するかに関係なく、CIパイプラインとローカルで同じように実行されます。 これにより、課題の比類のない再現性と開発者エクスペリエンスが得られます。

最後に、Testcontainers を使用すると、モックを使用せずに実際の依存関係を使用してテストを記述できるため、テストスイートの信頼性が高まります。 したがって、実用的なアプローチがお好きな方は、この記事で取り上げたすべてのテスト・タイプを最初から実行できるように、 Testcontainers Java SpringBoot QuickStartを確認してください。

さらに詳しく

フィードバック

0 の「Testcontainers:実際の依存関係を使用したテスト