🐁

PostgreSQL を使う go test を並列実行可能にしたい ... !!!

に公開

はじめに

テストケースが増えてくると PostgreSQL を使ったテストを並列で実行したくなるかと思います。
しかし、テストシナリオによってはあるシナリオが別のシナリオの弊害となるケースがあります。
例) 一覧取得の数が他のテストによるデータ操作によって変わってしまう。

今回の記事では PostgreSQL のテストを並列実行可能にするための工夫を Go 言語で実装する方法についてご紹介します。

対象テーブル

以下のテーブルを今回のテスト対象とします。

CREATE TABLE IF NOT EXISTS samples (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name TEXT NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);

また、以下のテーブル操作をテスト対象とします。

  • INSERT : レコードを作成する
  • SELECT : レコードをすべて取得する
  • DELETE : レコードをすべて削除する

テーブル操作のための Go 実装は以下のようなコードになります。

https://github.com/otakakot/sample-go-postgresql-test/blob/main/database/database.go

サンプルテストケース

あえてほかのテストにも影響しそうなケースを考えています。

  1. データが作成できる
  2. 期待したデータ数が取得できる
    • 事前に任意の数データを作成しておき作成した数だけデータが取得されることを確認する
  3. データがすべて削除されている
    • 事前に任意の数データを作成しておき作成したデータがすべて削除されることを確認する

これらのテストを同じデータベースを共有して実行してしまうと以下の不都合が発生します。

  • ほかのテストケースがデータを追加したことで数の整合性が合わなくなる
  • ほかのテストケースがデータを削除したことで数の整合性が合わなくなる

失敗するテスト

テストケースを実装したものは以下になります。

https://github.com/otakakot/sample-go-postgresql-test/blob/main/parallel/bad_test.go

用意したテストケースを特に工夫をすることなく並列に実行しようとすると失敗します。

4 つの実装方法

  1. 並列化は諦める
  2. テスト用のデータベースを都度作成する
  3. テスト用の PostgreSQL を都度起動する
  4. トランザクションに含める

1. 並列化は諦める

タイトルと反しますが、シンプルな解決策です。
実行時間がそんなに気にならないのであればこれで十分なケースもあると思います。

テストコード

https://github.com/otakakot/sample-go-postgresql-test/blob/main/parallel/serial_test.go

工夫

  • t.Parallel() は使わない
  • テスト実行前と実行後にテーブルデータを全削除する

Pros

  • テストのたびにデータベースや RDB コンテナを作る必要がない
  • テストコードのための追加実装が少ない

Cons

  • テストコード外で RDB(PostgreSQL) を準備し起動しておく必要がある
  • テスト実行時間が長くなる
  • テーブルレコードを全削除する実装が必要になる

2. テスト用のデータベースを都度作成する

ここでいうデータベースは PostgreSQL 内部に CREATE DATABASE で作成できるものを示します。
接続するデータベースをテストごとに分ける方法です。
都度作成することにはなりますが、ほかのテストが干渉してくることがなくなります。

テストコード

https://github.com/otakakot/sample-go-postgresql-test/blob/main/parallel/create_database_test.go

工夫

  • テストごとに CREATE DATABASE を実行する
  • テストごとにマイグレーションを実行する

Pros

  • テストコード外で RDB(PostgreSQL) を準備し起動しておく必要がある
  • テスト完了後はデータベースごと破棄できる

Cons

  • テスト用に CREATE DATABASE の仕組みを用意する必要がある
  • テスト用にマイグレーションの仕組みを用意する必要がある

3. テスト用の PostgreSQL を都度作成する

接続する PostgreSQL をテストごとに分ける方法です。
今回は PostgreSQL をコンテナで用意します。
コンテナ起動には Testcontainers を利用します。

https://zenn.dev/otakakot/articles/dc858e597d02f9

テストコード

https://github.com/otakakot/sample-go-postgresql-test/blob/main/parallel/create_postgresql_test.go

工夫

  • テストごとにコンテナを用意する
  • マイグレーションはファイルマウントにより実行する

Pros

  • テストコード内で RDB (PostgreSQL) が準備できる
  • テスト完了後はコンテナごと破棄できる

Cons

  • テスト用にコンテナ起動の仕組みを用意する必要がある
  • コンテナ起動に時間がかかる(テスト時間が長い)

4. トランザクションに含める

テストごとにトランザクションを利用することでほかのテストから隠蔽します。

テストコード

https://github.com/otakakot/sample-go-postgresql-test/blob/main/parallel/transaction_test.go

工夫

  • 構造体の依存を Pool でもトランザクションでも扱えるような interface にすることでテスト時にトランザクションに依存した構造体を扱えるようにする

https://github.com/otakakot/sample-go-postgresql-test/blob/main/database/transaction.go

Pros

  • テストのたびにデータベースや RDB コンテナを作る必要がない
  • テスト完了後はロールバックでデータをリセットできる

Cons

  • テストコード外で RDB(PostgreSQL) を準備し起動しておく必要がある
  • アプリケーションで不要な場合、テスト用にトランザクションを管理する必要がある

まとめ

直列実行 データベースによる並列化 PostgreSQL による並列化 トランザクションによる並列化
データクリーンアップ TRUNCATE 等の SQL 実行 データベースの破棄 コンテナの破棄 ロールバック
PostgreSQL の起動方法 docker などテストコード外での管理 docker などテストコード外での管理 Go コードによる管理 docker などテストコード外での管理
マイグレーション方法 テストコード外での管理が必要 テストコードでの管理が必要 コンテナ起動時のマウント テストコード外での管理が必要
必要なテスト用コード TRUNCATE 処理 データベース作成処理・マイグレーション実行処理 コンテナ起動処理 トランザクション処理

おわりに

いくつかパターンを紹介しましたが、どの手法が特に優れているかはないと思っています。
今回実装したコードは以下リポジトリの parallel ディレクトリに置いておきます。

https://github.com/otakakot/sample-go-postgresql-test

Discussion