テストコンテナを使用したGoアプリケーションのローカル開発

アプリケーションを構築するときは、プログラミング言語に関係なく、楽しい開発者エクスペリエンスを提供することが重要です。 この経験には、プロジェクトの開発ライフサイクルに関連するあらゆるタスクを実行するための優れたビルドツールが含まれます。 これには、コンパイル、リリース成果物のビルド、テストの実行が含まれます。

多くの場合、ビルドツールは、アプリケーションのランタイム依存関係の開始など、すべてのローカル開発タスクをサポートしているわけではありません。 その後、Makefile、シェルスクリプト、または外部のDocker Composeファイルを使用して手動で管理することを余儀なくされます。 これには、別の端末で呼び出したり、そのためのコードを維持したりすることが含まれる場合があります。 ありがたいことに、もっと良い方法があります。

この投稿では、Testcontainers for Goの使い方を紹介します。 Goアプリケーションのビルド中に実行時の依存関係を開始および停止する方法と、テストを簡単かつ一貫して実行する方法を学習します。 Fiber Web フレームワークを使用して非常にシンプルな Go アプリを構築し、PostgreSQL データベースに接続してユーザーを格納します。次に、Go の組み込み機能を活用し、Testcontainers for Go を使用してアプリケーションの依存関係を開始します。

テストコンテナを使用したgoアプリケーションのローカル開発

ソースコードは testcontainers-go-fiber リポジトリにあります。

Testcontainers for Go を初めて使用する場合は、このビデオを見て Testcontainers for Go の使用を開始してください。

手記: この投稿の目的は、アプリケーションの依存関係を開始する方法を示すことであり、依存関係と対話する方法を示すことではないため、ユーザー データベースと対話するコードは示しません。

ファイバーの紹介

ファイバーのウェブサイトから:

Fiber は、Go 用の最速の HTTP エンジンである Fasthttp 上に構築された Go Web フレームワークです。 これは、メモリ割り当てとパフォーマンスを念頭に置いて、迅速な開発を容易にするように設計されています。

なぜファイバーなのか? Go で HTTP を操作するためのフレームワークには、gin や gobuffalo など、さまざまなものがあります。 また、多くの Gopher は Go の標準ライブラリのパッケージに直接 net/http 収まっています。 結局のところ、フレームワークのどのライブラリを選択しても、ここで示す内容とは無関係です。

デフォルトのファイバーアプリケーションを作成しましょう。

package main

import (
   "log"
   "os"

   "github.com/gofiber/fiber/v2"
)

func main() {
   app := fiber.New()

   app.Get("/", func(c *fiber.Ctx) error {
       return c.SendString("Hello, World!")
   })

   log.Fatal(app.Listen(":8000"))
}

前述したように、アプリケーションはPostgresデータベースに接続してユーザーを保存します。 アプリケーション間で状態を共有するために、アプリを表す新しい型を作成します。 この App 種類には、ファイバー アプリケーションに関する情報と、ユーザー データベースの接続文字列が含まれます。

// MyApp is the main application, including the fiber app and the postgres container
type MyApp struct {
   // The name of the app
   Name string
   // The version of the app
   Version string
   // The fiber app
   FiberApp *fiber.App
   // The database connection string for the users database. The application will need it to connect to the database,
   // reading it from the USERS_CONNECTION environment variable in production, or from the container in development.
   UsersConnection string
}

var App *MyApp = &MyApp{
   Name:            "my-app",
   Version:         "0.0.1",
   // in production, the URL will come from the environment
   UsersConnection: os.Getenv("USERS_CONNECTION"),
}

func main() {
   app := fiber.New()

   app.Get("/", func(c *fiber.Ctx) error {
      return c.SendString("Hello, World!")
   })

   // register the fiber app
   App.FiberApp = app

   log.Fatal(app.Listen(":8000"))
}

デモンストレーションの目的で、このパッケージを使用して main 、Postgres データベース内のユーザーへのアクセスを定義します。 実際のアプリケーションでは、このコードはパッケージに含まれ main ていません。

ローカル開発用のアプリケーションを実行すると、次のようになります。

Go用のTestcontainers

Testcontainers for Go は、Go テストから Docker コンテナーを開始および停止できる Go ライブラリです。 これにより、独自のコンテナを定義する方法が提供されるため、必要なコンテナ を開始して構成できます 。 また、アプリケーションの依存関係を開始するために使用できる Goモジュール の形式で事前定義されたコンテナのセットも提供します。

したがって、Testcontainersを使用すると、データベース、メッセージブローカー、またはDockerコンテナ内の他の種類の依存関係と対話できるため、抽象的な方法で依存関係を操作できます。

開発モードの依存関係の開始

そのためのライブラリができたので、アプリケーションの依存関係を開始する必要があります。 ここで言っているのは、アプリケーションを構築するローカルなエクスペリエンスのことを言っていることを思い出してください。 そのため、本番環境ではなく、特定のビルド条件下でのみ依存関係を開始する必要があります。

Go ビルドタグ

Goは、ビルド条件を定義するために使用できるビルドタグを定義する方法を提供します。 Go ファイルの先頭にコメントの形式でビルドタグを定義できます。 たとえば、次のように build dev タグを定義できます。

// +build dev
// go:build dev

このビルドタグをファイルに追加すると、ビルドタグがコマンドにgo build渡されたときにdevのみファイルがコンパイルされ、リリースアーティファクトには到達しません。ツールチェーンの go 威力は、このビルドタグが go ツールチェーンを使用するすべてのコマンド ( など) go runに適用されることです。 したがって、このビルドタグは、アプリケーション go run -tags dev .を実行するときに引き続き使用できます。

Go init 関数

Goの関数は init 、関数の前に main 実行される特別な関数です。 Go ファイルで init 関数を次のように定義できます。

func init() {
   // Do something
}

これらは決定論的な順序で実行されるわけではないため、関数を定義する init 際にはこの点を考慮してください。

この例では、Go アプリケーションのローカル開発エクスペリエンスを向上させたいので、build タグでdev保護されたファイルでdev_dependencies.go関数を使用しますinit。そこから、アプリケーションの依存関係 (この場合はユーザーの PostgreSQL データベース) を開始します。

Testcontainers for Go を使用して、この Postgres データベースを起動します。 これらの情報 dev_dependencies.go をすべてファイルにまとめてみましょう。

//go:build dev
// +build dev

package main

import (
   "context"
   "log"
   "path/filepath"
   "time"

   "github.com/jackc/pgx/v5"
   "github.com/testcontainers/testcontainers-go"
   "github.com/testcontainers/testcontainers-go/modules/postgres"
   "github.com/testcontainers/testcontainers-go/wait"
)

func init() {
   ctx := context.Background()

   c, err := postgres.RunContainer(ctx,
       testcontainers.WithImage("postgres:15.3-alpine"),
       postgres.WithInitScripts(filepath.Join(".", "testdata", "dev-db.sql")),
       postgres.WithDatabase("users-db"),
       postgres.WithUsername("postgres"),
       postgres.WithPassword("postgres"),
       testcontainers.WithWaitStrategy(
           wait.ForLog("database system is ready to accept connections").
               WithOccurrence(2).WithStartupTimeout(5*time.Second)),
   )
   if err != nil {
       panic(err)
   }

   connStr, err := c.ConnectionString(ctx, "sslmode=disable")
   if err != nil {
       panic(err)
   }

   // check the connection to the database
   conn, err := pgx.Connect(ctx, connStr)
   if err != nil {
       panic(err)
   }
   defer conn.Close(ctx)

   App.UsersConnection = connStr
   log.Println("Users database started successfully")
}

コンテナーは c 、Testcontainers for Go を使用して定義され、開始されます。 以下を使用しています。

  • WithInitScriptsデータベースとテーブルを作成する SQL スクリプトをコピーして実行するオプション。このスクリプトは、 testdata フォルダーにあります。
  • WithWaitStrategyデータベースが接続を受け入れる準備が整うのを待ち、データベースログをチェックするオプション。
  • WithDatabaseWithUsernameおよびWithPasswordデータベースを構成するためのオプション。
  • ConnectionString開始されたコンテナーからデータベースへの接続文字列を直接取得するメソッド。

変数は App 、前に定義した型で、アプリケーションを表します。 この種類には、ファイバー アプリケーションに関する情報と、ユーザー データベースの接続文字列が含まれていました。 そのため、コンテナーが起動した後は、起動したコンテナーから直接データベースへの接続文字列を入力します。

今のところ大丈夫です! Go の組み込み機能を利用して、フラグがコマンドに追加されgo runたときに-tags devのみ、ファイルでdev_dependencies.go定義された init 関数を実行しました。

このアプローチでは、アプリケーションとその依存関係の実行には 1 つのコマンドが必要です。

go run -tags dev .

Postgres データベースが起動し、テーブルが作成されていることがわかります。 また、変数には App 、ファイバー アプリケーションに関する情報とユーザー データベースの接続文字列が入力されていることもわかります。

開発モードの依存関係の停止

依存関係が開始されたので、ビルドタグがコマンドに go run 渡された場合にのみ、アプリケーションが停止されたときにそれらを停止する必要があります。

ビルドタグで行ったことを再利用して、ビルドタグがコマンドにgo run渡されたときdevにのみアプリケーション自体を停止する前に、アプリケーションの依存関係を停止するグレースフルシャットダウンを登録します。

Fiberアプリはそのままで、ファイルを更新する dev_dependencies.go だけで済みます。

//go:build dev
// +build dev

package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "os/signal"
    "path/filepath"
    "syscall"
    "time"

    "github.com/jackc/pgx/v5"
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
    "github.com/testcontainers/testcontainers-go/wait"
)

func init() {
    ctx := context.Background()

    c, err := postgres.RunContainer(ctx,
        testcontainers.WithImage("postgres:15.3-alpine"),
        postgres.WithInitScripts(filepath.Join(".", "testdata", "dev-db.sql")),
        postgres.WithDatabase("users-db"),
        postgres.WithUsername("postgres"),
        postgres.WithPassword("postgres"),
        testcontainers.WithWaitStrategy(
            wait.ForLog("database system is ready to accept connections").
                WithOccurrence(2).WithStartupTimeout(5*time.Second)),
    )
    if err != nil {
        panic(err)
    }

    connStr, err := c.ConnectionString(ctx, "sslmode=disable")
    if err != nil {
        panic(err)
    }

    // check the connection to the database
    conn, err := pgx.Connect(ctx, connStr)
    if err != nil {
        panic(err)
    }
    defer conn.Close(ctx)

    App.UsersConnection = connStr
    log.Println("Users database started successfully")

    // register a graceful shutdown to stop the dependencies when the application is stopped
    // only in development mode
    var gracefulStop = make(chan os.Signal)
    signal.Notify(gracefulStop, syscall.SIGTERM)
    signal.Notify(gracefulStop, syscall.SIGINT)
    go func() {
        sig := <-gracefulStop
        fmt.Printf("caught sig: %+v\n", sig)
        err := shutdownDependencies()
        if err != nil {
            os.Exit(1)
        }
        os.Exit(0)
    }()
}

// helper function to stop the dependencies
func shutdownDependencies(containers ...testcontainers.Container) error {
    ctx := context.Background()
    for _, c := range containers {
        err := c.Terminate(ctx)
        if err != nil {
            log.Println("Error terminating the backend dependency:", err)
            return err
        }
    }

    return nil
}

このコードでは、関数の init 下部で、データベース接続文字列を設定した直後に、正常なシャットダウンを処理するために を開始し goroutine ています。 また、定義 SIGTERMSIGINT シグナルにも耳を傾けています。 シグナルがチャネルに gracefulStop 入力されると、 shutdownDependencies アプリケーションの依存関係を停止するためにヘルパー関数が呼び出されます。 このヘルパー関数は、データベースコンテナの Testcontainers for Go の Terminate メソッドを内部的に呼び出し、コンテナをシグナルで停止させます。

このアプローチで特に優れているのは、作成された環境がいかに動的であるかです。 Testcontainers は、並列化を可能にするために余分な労力を費やし、コンテナーを高レベルの使用可能なポートにバインドします。 これは、開発モードがテストの実行と衝突しないことを意味します。 または、アプリケーションの複数のインスタンスを問題なく実行することもできます。

ねえ、本番では何が起こるの?

アプリは環境からデータベースへの接続を初期化しているため、次のようになります。

var App *MyApp = &MyApp{
   Name:            "my-app",
   Version:         "0.0.1",
   DevDependencies: []DevDependency{},
   // in production, the URL will come from the environment
   UsersConnection: os.Getenv("USERS_CONNECTION"),
}

その値がローカル開発のカスタムコードによって上書きされることを心配する必要はありません。 UsersConnection は、ここで示したものはすべてビルド タグによって保護されているため、 dev 設定されません。

手記:Gin または net/http を直接使用していますか?ここで説明したすべてのもの、つまり init ランタイムの依存関係を開始および正常なシャットダウンするための関数とビルドタグから直接恩恵を受けることができます。

結論

この投稿では、Testcontainers for Goを使用して、アプリケーションのビルドとテストの実行中にアプリケーションの依存関係を開始および停止する方法を学びました。 そして、活用する必要があるのは、Go言語と go ツールチェーンの組み込み機能だけでした。

その結果、アプリケーションをビルドして実行しながら、アプリケーションの依存関係を開始できます。 また、アプリケーションが停止すると、それらを停止できます。 これは、Makefile、シェルスクリプト、または外部の Docker Compose ファイルで依存関係を開始する必要がないため、ローカル開発エクスペリエンスが向上することを意味します。 そして最も重要なことは、開発モードでのみ発生し、フラグを -tags dev コマンドに go run 渡すことです。

さらに詳しく