Skip to content

テスト環境の分離(DB/Garage/マイグレーション)

何のためのものか

以前のマイルストーンで、backend/db/schema/001_river.sqlが実際に必要なスキーマと乖離していたバグを発見した。原因は、スキーマ適用がdocker-entrypoint-initdb.d(Postgresコンテナの初回起動時にしか走らない仕組み)に依存していたため、既存のデータディレクトリが「初期化済み」である限りドリフトに誰も気づけなかったこと。

これと合わせて、scripts/e2e-smoke.shによるE2E検証が開発用のPostgres/Garageに直接読み書きしており、実行のたびに開発データにゴミが残る問題もあった。

この2つを解消するため、マイグレーションツール(goose)の導入と、開発/テストでDB・Garageバケットを分離する仕組みを整備した。

決定事項

DB分離: 別コンテナではなく別DB名

同一Postgresインスタンス内にcocot_testという別のDBを作る。別コンテナ方式やテストごとのトランザクションロールバック方式は採らなかった。Riverの非同期ジョブ処理やE2E検証(実際にHTTPリクエストを送り、Workerの完了をポーリングで待つ)は、テストの外側でコミットされた状態を前提にしており、ロールバック方式とは相性が悪いため。

マイグレーションツール: goose

pressly/gooseを採用した。golang-migrateも検討したが、既存の連番SQLファイル(backend/db/schema/001_*.sql〜)への移行コストが低い(各ファイルの先頭に-- +goose Upを1行足すだけでよい)ことを決め手とした。

down migrationは今回書いていない。cocot_testは「DROP→CREATE→goose up」で毎回作り直す運用(後述のmise run db-test-reset)にすることで、ロールバックの必要性自体をなくしている。開発用DB(cocot)のdown migration整備は別途のフォローアップとする。

gooseの適用範囲は、テスト用DBだけでなく開発用DBのスキーマ適用にも一本化した。compose.ymlpostgresサービスから./backend/db/schema:/docker-entrypoint-initdb.dのマウントを削除し、mise run db-migrateで明示的に適用する運用に変えている。これが冒頭のドリフトバグの根本原因を構造的に解消する変更になる。

導入はmise.toml[tools]"go:github.com/pressly/goose/v3/cmd/goose"を追加する形で行った。mise installでホストにgooseのバイナリが入る。

Garage分離: 別インスタンスではなく別バケット名

同一Garageインスタンス内にaudio-tmp-testaudio-permanent-testという別バケットを作る。DB分離と同じ理由(インスタンスを増やすとcompose構成が複雑になる一方、バケット名を変えるだけで実用上十分な分離が得られる)による。

テスト用バケットの作成・アクセスキーへの権限付与は、手動作業ではなくmise-tasks/garage-buckets-testにべき等な形で組み込んである(存在確認してからgarage bucket create、権限付与は毎回実行しても安全)。

切り替え方式: DOTENV_FILE環境変数

アプリコード自身に「test/devモード」の分岐は作っていない。godotenv.Load()godotenv.Overload(dotenvFile)に変え、dotenvFileは環境変数DOTENV_FILE(未設定時.env)で決まるようにした(backend/cmd/app/main.gobackend/cmd/worker/main.go)。LoadではなくOverloadにしているのは、compose.ymlenv_fileがすでにコンテナに注入した値より.env.testの値を確実に優先させるため(Loadは既存の環境変数を上書きしない)。

compose.yml自体は変更していない。postgres/garageコンテナはdev/testで共有する。app/workerも同じサービス定義をdocker compose run --rm -e DOTENV_FILE=.env.test <service>で一時的に流用する(mise run test-e2e、後述)。

罠: .env.testは「差分だけ」で足りない場合がある

.env.testに書かれていない変数は、Dockerのenv_fileがすでに注入した値がそのまま使われる。appサービスのenv_filebackend/cmd/app/.env自身なので、backend/cmd/app/.env.testはDB名・バケット名など差分だけの記述で問題ない。

しかしworkerサービスのenv_filebackend/cmd/app/.envを指しており、backend/cmd/worker/.env自身はDockerのenv_fileとして一度も参照されないcompose.yml参照)。通常時(DOTENV_FILE未設定)はworker自身のmain.goが起動時にgodotenv.Overload(".env")でworker自身の.envCOCOT_STORAGE_S3_*など)を読み込むことでこれを補っているが、DOTENV_FILE=.env.testにするとworker自身の.envは一切読まれず.env.testだけが有効になる。そのためbackend/cmd/worker/.env.testは差分だけではCOCOT_STORAGE_S3_*が空文字列になり、resolve endpoint: endpoint rule error, Custom endpoint `` was not a valid URIのようなエラーでWorkerの変換が失敗し続ける(Riverが自動リトライするため、症状としては「transcodingのまま進まない」ように見える)。

backend/cmd/worker/.env.testbackend/cmd/worker/.envの完全なコピーにし、DATABASE_URLTMP_AUDIO_BUCKETCOCOT_STORAGE_BUCKETだけ書き換えること。

タスクランナー: mise tasks

追加の依存を増やさず、Windows環境でも動くことを優先し、mise tasksを採用した。mise-tasks/配下に実行可能なシェルスクリプトを置く方式(mise.toml[tasks]インライン記法より、複数ステップのオーケストレーションに向くため)。

タスク内容
mise run setupmise installdb-migrategarage-buckets-testをまとめて実行する、新規開発者向けの一発セットアップ
mise run db-migrate開発用DB(cocot)に未適用のマイグレーションを適用する
mise run db-test-resetcocot_testをDROP→CREATEしてから全マイグレーションを適用する
mise run garage-buckets-testaudio-tmp-testaudio-permanent-testをべき等に作成し、tus-uploaderキーに読み書き権限を付与する
mise run test-unitcd backend && go test ./...(fakeベース、DB/Garageに触れない)
mise run test-e2eテストDB/バケットの準備→開発用app/workerコンテナを一時停止→docker compose run --rm -d -e DOTENV_FILE=.env.test <service>でapp/workerを一時起動→ヘルスチェック(app)・起動ログ(worker)待ち→scripts/e2e-smoke.sh実行→コンテナ停止、を一括で行う

test-e2eはapp・worker両方をdocker compose run --rm -d --service-ports -e DOTENV_FILE=.env.test <service>で起動する(--service-portsはappの8000番ポート公開のため)。開発用のapp/workerサービスをそのまま流用する一時的なワンオフコンテナで、docker compose upで常駐しているものとは別物(--rmで終了時に自動削除される)。開発用コンテナが起動中だとポート8000が競合するため、開始時にdocker compose stop app workerを挟んでいる(test-e2e終了後に開発用コンテナを再度使う場合はdocker compose up app workerで立ち上げ直すこと)。

workerはffmpegエンコード込みで実処理に数十秒かかることがあるため、appの/healthが200を返すまでとは別に、workerのログにworker started, waiting for jobsが出るまで待ってからscripts/e2e-smoke.shを実行する。

scripts/e2e-smoke.shの変更

uploadsテーブルを直接照会する箇所(Workerの変換完了をポーリングで待つ部分)がpsql -U cocot -d cocotとDB名をハードコードしていたため、PG_DATABASE環境変数(デフォルトcocot)から取れるようにした。mise run test-e2eはここにcocot_testを渡す。

使い方

新規セットアップ:

Terminal window
docker compose up -d postgres garage
mise run setup

テスト実行:

Terminal window
FIREBASE_API_KEY=... \
USER_A_EMAIL=... USER_A_PASSWORD=... \
USER_B_EMAIL=... USER_B_PASSWORD=... \
mise run test-e2e

scripts/e2e-smoke.sh単体の使い方(前提条件・検証内容の詳細)はtesting/e2e-smoke-testingを参照。

対象外・フォローアップ

  • 開発用DB(cocot)に対するdown migrationの整備。
  • CI(GitHub Actionsなど)への組み込み。今回はローカルでのタスクランナー整備までが範囲。