Skip to content

Testcontainer実装の詳細

概要

このドキュメントでは、テスト環境をSQLiteからTestcontainersとPostgreSQLに移行した詳細について説明します。この変更により、テストは本番環境で使用されるのと同じデータベースタイプに対して実行されるようになり、より信頼性の高い現実的なテスト結果を提供します。

Testcontainersとは?

Testcontainersは、一般的なデータベース、Seleniumウェブブラウザ、またはDockerコンテナで実行できるその他のものの軽量で使い捨てのインスタンスを提供するライブラリです。特に統合テストに有用で、モックやインメモリの代替手段ではなく、実際の依存関係に対してテストを実行できます。

なぜSQLiteからTestcontainersに移行したのか

以前、テストではSQLiteのインメモリデータベースを使用していました:

// SQLiteを使用した古いアプローチ
func setupDatabase() (*gorm.DB, func()) {
    // テスト目的でsqliteを使用
    database, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
    if err != nil {
        panic("Failed to connect to database")
    }

    // モデルの自動マイグレーション
    err = database.AutoMigrate(&postgres.Product{}, &postgres.Seller{})
    if err != nil {
        panic("Failed to migrate database")
    }

    // テーブルをクリーンアップする関数
    cleanup := func() {
        database.Exec("DELETE FROM sellers")
        database.Exec("DELETE FROM products")
    }

    return database, cleanup
}

このアプローチはシンプルで高速でしたが、いくつかの制限がありました:

  1. 異なるデータベースエンジン: SQLiteとPostgreSQLは動作、機能、SQLの方言が異なります。SQLiteでパスするテストが本番環境のPostgreSQLで失敗する可能性があります。
  2. スキーマの互換性: PostgreSQLで使用されるスキーマ機能の一部はSQLiteでサポートされていない場合があります。
  3. 並行処理: PostgreSQLとSQLiteは並行操作の処理方法が異なります。
  4. 本番環境に近い環境: テストは理想的には本番環境にできるだけ近い環境で実行されるべきです。

Testcontainersの実装方法

現在、テスト用にPostgreSQLコンテナをスピンアップするためにTestcontainersを使用しています:

func setupDatabase(t *testing.T) (*gorm.DB, func()) {
    ctx := context.Background()

    // PostgreSQLコンテナの定義
    pgReq := testcontainers.ContainerRequest{
        Image:        "postgres:15-alpine",
        ExposedPorts: []string{"5432/tcp"},
        Env: map[string]string{
            "POSTGRES_USER":     "postgres",
            "POSTGRES_PASSWORD": "postgres",
            "POSTGRES_DB":       "testdb",
        },
        WaitingFor: wait.ForLog("database system is ready to accept connections").
            WithOccurrence(2).WithStartupTimeout(5 * time.Second),
    }

    // PostgreSQLコンテナの起動
    pgContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
        ContainerRequest: pgReq,
        Started:          true,
    })
    if err != nil {
        t.Fatalf("Failed to start PostgreSQL container: %s", err)
    }

    // コンテナのホストとポートの取得
    host, err := pgContainer.Host(ctx)
    if err != nil {
        t.Fatalf("Failed to get PostgreSQL container host: %s", err)
    }

    port, err := pgContainer.MappedPort(ctx, "5432")
    if err != nil {
        t.Fatalf("Failed to get PostgreSQL container port: %s", err)
    }

    // 接続文字列の作成
    dsn := fmt.Sprintf("host=%s port=%s user=postgres password=postgres dbname=testdb sslmode=disable", host, port.Port())

    // PostgreSQLデータベースへの接続
    database, err := gorm.Open(pgdriver.Open(dsn), &gorm.Config{})
    if err != nil {
        t.Fatalf("Failed to connect to database: %s", err)
    }

    // モデルの自動マイグレーション
    err = database.AutoMigrate(&postgres.Product{}, &postgres.Seller{})
    if err != nil {
        t.Fatalf("Failed to migrate database: %s", err)
    }

    // クリーンアップ関数
    cleanup := func() {
        // データベースのクリーンアップ
        database.Exec("DELETE FROM sellers")
        database.Exec("DELETE FROM products")

        // PostgreSQLコンテナの停止と削除
        if err := pgContainer.Terminate(ctx); err != nil {
            t.Fatalf("Failed to terminate container: %s", err)
        }
    }

    return database, cleanup
}

この実装は:

  1. 公式PostgreSQL Dockerイメージを使用してPostgreSQLコンテナを作成
  2. データベースの環境変数を設定
  3. データベースが接続を受け付ける準備ができるのを待機
  4. コンテナのホストとポートを取得
  5. 接続文字列を作成してデータベースに接続
  6. データベーススキーマをマイグレーション
  7. データベース接続とクリーンアップ関数を返す:
  8. すべてのレコードを削除してデータベースをクリーンアップ
  9. コンテナを終了

テストでのTestcontainerの使用方法

以下は、テストでtestcontainerセットアップを使用する例です:

func TestGormProductRepository_Save(t *testing.T) {
    gormDB, cleanup := setupDatabase(t)
    defer cleanup()

    repo := postgres.NewGormProductRepository(gormDB)

    seller := getPersistedSeller(gormDB)
    validatedSeller, _ := entities.NewValidatedSeller(&seller.Seller)

    product := entities.NewProduct("TestProduct", 9.99, *validatedSeller)
    validProduct, _ := entities.NewValidatedProduct(product)

    _, err := repo.Create(validProduct)
    if err != nil {
        t.Errorf("Unexpected error during save: %s", err)
    }
}

各テストは: 1. コンテナ内に新しいPostgreSQLデータベースをセットアップ 2. テスト操作を実行 3. すべてのデータを削除し、コンテナを終了することで自身の後処理を行う

Testcontainersを使用する利点

  1. 本番環境に近い環境: テストは本番環境で使用されるのと同じデータベースタイプに対して実行されます。
  2. 分離: 各テストは独自の分離されたコンテナで実行され、テスト間の干渉を防ぎます。
  3. 現実的なテスト: テストはPostgreSQLのすべての機能を使用でき、本番環境との互換性を確保します。
  4. 信頼性: テストが合格すると、コードが本番環境で動作するという確信が高まります。
  5. ポータビリティ: テストはローカルのPostgreSQLインストールを必要とせず、Dockerがあれば任意のマシンで実行できます。

システム要件

Testcontainersを使用するには、以下が必要です:

  1. Dockerがインストールされ、実行されていること
  2. Go 1.19以降
  3. testcontainers-goライブラリ: go get github.com/testcontainers/testcontainers-go

結論

SQLiteからTestcontainersとPostgreSQLに移行することで、テストの信頼性と現実性が向上しました。コンテナの起動時間によりテストの実行が若干遅くなる可能性がありますが、本番環境に近い環境でテストを行うメリットはこのデメリットを上回ります。