🙂

Testcontainersを利用してテストを改善しよう

に公開

はじめに

どうもk1mu21です!

テスト時にある環境が必要ですが、タイミング悪く落ちてしまってテストが必ず失敗してしまうといったことはありがちではないでしょうか?
そのためシステムの返り値をモック化してテストコードを書くような状況になりがちかと思います

また昨今、AI AgentがVibeCordingをする時代にもなってさらにテストの重要度が上がっているので変更に強いテストを作成する必要があります

今回はそんな状況に対してTestcontainersを利用してテスト環境をある程度改善する方法をご紹介したいと思います

Testcontainersとは?

Testcontainersは、Dockerコンテナで動作する軽量なインスタンスを提供するオープンソースライブラリになります

https://testcontainers.com/?language=java

主要言語で使えるので導入はそこまでネックにならないと思います

https://testcontainers.com/modules/?language=go

コンテナ化されていない場合でも、Testcontainersが提供する各種モジュール(データベースなど)を利用して、依存するサービスをコンテナとして立ち上げることが可能です

使い方

  • Goを利用した場合の例になります
  • ElasticSearchのコンテナを利用
elasticsearch_test.go
...

func TestCreateElasticsearch(t testing.T) {
    ctx := context.Background()

    // testContainersを使ってElasticsearchコンテナを起動
    esContainer, err := tces.Run(
        ctx,
        "docker.elastic.co/elasticsearch/elasticsearch:8.9.0",
        // 環境変数等を設定
        testcontainers.WithEnv(map[string]string{
            "discovery.type":         "single-node",
        }),
        // Elasticsearchコンテナを起動する際の待機戦略
        // Elasticsearchコンテナが起動して、9200番ポートで HTTP ステータス200を返すまで最大2分待つ
        testcontainers.WithWaitStrategy(
            wait.ForHTTP("/"). 
                        WithPort("9200/tcp").
                        WithStatusCodeMatcher(func(code int) bool { return code == http.StatusOK }).
                        WithStartupTimeout(2 * time.Minute),
        ),
    )
    if err != nil {
        t.Fatalf("failed to start elasticsearch container: %v", err)
    }
    t.Cleanup(func() {
        _ = testcontainers.TerminateContainer(esContainer)
    })

    // コンテナのアドレス、ポートを取得
    if _ , err := esContainer.Host(ctx); err != nil {
        t.Fatal(err)
    }
    if _ , err := esContainer.MappedPort(ctx, "9200/tcp"); err != nil {
        t.Fatal(err)
    }

    ...
}

https://testcontainers.com/modules/elasticsearch/?language=go

このElasticsearchのモジュールを使うと簡単にElasticsearch環境を設定することができます

そしてテストを実行すると、設定したコンテナに対してクライアントの作成するテストや、go-elasticsearchを利用したクエリの発行のテスト等が実行できるようになります

これによって1つの環境だけでElasticsearchのテストが完結できるようになりました

メリット

テスト時にコンテナ化されたシステムを立ち上げることができるので依存を減らせる

  • あるシステムが落ちていて同通ができないという状況でも、テスト環境内だけで完結できるので環境起因のテストの失敗がなくなります
    • テストの信頼性が上がります
    • 1つの環境で収まるので、結合テストも実施しやすくなるのもありがたい点だと思います

コンテナ化するだけでテスト環境に利用できる

  • テスト用に常に立ち上げてる環境があったとしても、コンテナ化すればTestcontainarsとして利用できるのでサーバ等のランニングコストを減らすことができます

モックを減らせる

  • システムが落ちている場合を考慮した設計にしなくても良くなるためモックが減らせます
    • 実際の返り値等でテストで使えるようになるのが利点です
    • モックが減るのでAI Agent等のコード修正に強いテストになると思います

デメリット

コンテナを起動するので実行が重い

  • Testcontainersはコンテナを利用したものになるので、imageによっては起動にとても時間がかかってしまいます
    • GitHub Actions上でテストを実行している場合、実行時間が増えるほど課金量も増えてしまうので考えた上で実行する必要があります
    • 例としてDBではイメージを持って来るだけではなく、データを入れたり実際の環境に近づけた設定を入れたりするだけで初期化に時間がかかりそうという想像が容易にできると思います

対策方法

  • 一つの対策方法としてTestcontainersを利用したテストを別のジョブとして切り出すことができます
    • 1つのジョブにまとまっているとTestcontainersが必要ないテスト+必要なテストと時間が合計でかかってしまいます
    • ジョブを分けることで最大実行時間を減らすことが可能です
test.yaml
name: Run unit tests
    run: go test -v $(go list ./... | grep -v -e /conf)

# testcontainersを使ったテストを別jobとして切り出してる(タイムアウト3分設定)
name: Run testcontainers
    run: go test -v -run ^TestCreateElasticsearch$ -timeout 3m

結局はテスト環境

  • 結局はテスト環境内で立てている環境であるため、本番環境とは差異がある
    • DBとかだとストレージ設定やIOPS、メモリ量など...
    • どこまでTestcontainersで担保するのかを考えてテストをする必要があります

まとめ

Testcontainersはテスト内でシステム依存の部分をモック化している場合等に有効です

さらにテストが重要になってきている時代なので積極的に採用した方がいいと思いました

重いのが正直ネックだと思っていますが、それ以上にテストの信頼性を上げることができるのは大きなリターンだと思うのでぜひ使ってみて下さい!

GitHubで編集を提案

Discussion