@testcontainers/postgresqlを使ってみる
TypeScriptテストコードから直接PostgreSQL Dockerコンテナをスピンアップして管理するためのプログラム的な方法を提供します。コンテナを構成するためのメソッド(データベース名、ユーザー名、パスワード、ポート、イメージバージョンなど)を提供します。テスト開始前にコンテナが完全に初期化されることを保証するためのさまざまな待機戦略をサポートします。必要に応じて、複数のコンテナ間のネットワーク構成と相互作用を容易にします。Ryukを使用して、テスト実行後にコンテナを自動的にクリーンアップします。
@testcontainers/postgresqlモジュールは、getDatabase()、getUsername()、getPassword()、getHost()、getPort()、getConnectionUri()などのヘルパーメソッドを提供することで、PostgreSQLコンテナとの相互作用を簡素化します。これらのメソッドを使用すると、PostgreSQLインスタンスに必要な接続の詳細を簡単に取得できます。Testcontainersは、PostgreSQLコンテナをカスタマイズするためのwithDatabase()、withUsername()などのビルダーメソッドを公開しています。これにより、特定のテストニーズに合わせてコンテナを柔軟に構成できます。
めちゃ便利やん。
ここでは、Testcontainersの主要な特徴と利点についてまとめた情報を整形してご紹介します。
Testcontainersの概要と利点
基本的な機能
Testcontainersを使うと、テストのセットアップ段階でPostgreSQLなどのコンテナを自動的に起動し、テスト完了後に自動でクリーンアップする流れを実現できます。従来の手動Docker操作と比べて、以下の点が改善されます:
- コンテナのライフサイクル(起動・停止・破棄)を自動管理
- テスト実行前にコンテナの準備が完了していることを保証
- テストごとにクリーンな環境を提供
採用状況と一般性
- Node.js/TypeScript向けのTestcontainersは2024年現在かなり普及
- もともとJava向けに開発されましたが、公式にNode.js版も提供されており、TypeScript対応も充実
- 本番環境に近いセットアップのために高く評価されている
- 各種サービス向けの公式モジュール(PostgreSQLモジュールなど)が用意されている
- Node.js向けの
@testcontainers/postgresqlは週約25万ダウンロードを記録
主なメリット
-
本番に近い環境:
- 実際のPostgreSQL公式Dockerイメージを使用するため挙動の差異が少なく信頼性が高い
- トランザクションやエクステンションも本物で検証可能
-
テストの隔離:
- コンテナ単位でDBインスタンスを用意でき、テストごと・ワーカーごとに独立したDBを持たせやすい
- Vitestのような並列実行環境でも各ワーカーに個別のコンテナを割り当て可能
-
自動ライフサイクル管理:
- テストコード内でコンテナの起動・破棄を自動制御
- 依存サービスの起動完了待機も適切に処理
-
TypeScript/Vitest統合の容易さ:
- Promise/awaitベースでVitestの
beforeAllなどと連携可能 - 型定義も提供されており、モジュール経由の高レベルAPIで簡単に設定可能
- Promise/awaitベースでVitestの
-
CI環境との親和性:
- Dockerさえ利用可能なら、CI上でもローカルと同じコードでコンテナ起動可能
- GitHub ActionsのUbuntuランナーなどですぐに利用できる
デメリット・注意点
-
Docker依存とオーバーヘッド:
- Dockerデーモンが動作する環境が必要
- コンテナ起動には数秒程度のオーバーヘッドが発生
- テスト数が非常に多い場合は時間がかかる可能性がある
-
リソース使用:
- コンテナ実行によるメモリやストレージの消費
- 多数のコンテナ同時実行時はリソース制限に注意が必要
- CIでのポート競合などに注意
-
学習コスト:
- Testcontainers独自のAPIやコンセプトを学ぶ必要がある
- ただし公式ドキュメントやコミュニティが充実している
Testcontainersは全体として、テスト環境の自動化と再現性の向上に大きく貢献するツールであり、特にPostgreSQLなどのデータベースを使った統合テストにおいて有用です。
導入
pnpm add -D @testcontainers/postgresql
it("should connect and return a query result", async () => {
const container = await new PostgreSqlContainer().start();
const client = new Client({
host: container.getHost(),
port: container.getPort(),
database: container.getDatabase(),
user: container.getUsername(),
password: container.getPassword(),
});
await client.connect();
const result = await client.query("SELECT 1");
expect(result.rows[0]).toEqual({ "?column?": 1 });
await client.end();
await container.stop();
});