Cloud Spanner (以下、Spannerと呼ぶ) とは、Google Cloud Platformで利用可能なフルマネージドデータベースです。高い可用性がありながらトランザクションを自動的に処理できるため、弊社(メルカリ)でも幅広く活用されています。またその中で、サーバーのテストにSpannerの処理を組み込みたい場面が多々存在します。
以前までは、テスト用のSpannerインスタンスを用意しておいて、テストに利用するのが一般的でした。しかし、運用コストや手軽さを加味すると、ローカル上で動かせるエミュレータのほうが扱いやすいかもしれません。
Cloud Spanner Emulator(以下、Spanner Emulatorと呼ぶ)は、2020年4月にbeta版がリリースされました。バイナリからだけでなく、Google Cloud SDKやDockerを用いてエミュレータを立ち上げられるため、導入コストも削減できます。
本記事では、実際にSpanner Emulatorを利用したGoのテストについて、サンプルコードを付随しながら解説していきます(サンプルコードのリポジトリ)。
Spanner Emulatorの導入
まず、Spanner Emulatorの環境を準備するところから簡単に紹介します。Spanner Emulatorを起動する方法はいくつか存在します[1]。
本記事では、例としてDockerを用いてSpanner Emulatorを起動します。Spanner Emulatorのイメージは、gcr.io/cloud-spanner-emulator/emulator
で公開されており、こちらをpullすることで簡単に起動できます。
docker run -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator:latest
GoからSpanner Emulatorを呼び出す際は、基本的にcloud上にあるSpannerにアクセスするときと同じで、Google Cloud Client Libraryを使用します。cloudと異なる点として、環境変数でSPANNER_EMULATOR_HOST
を指定することで、Spanner Emulatorにアクセスすることができます。
GoのテストでSpanner Emulatorを使う
本記事でご紹介するデータベースのライフサイクルは、次の図のようになります(1つのパッケージ内でのライフサイクルを表しています)。
GoではTestMainを用いることで、パッケージ内でのテストの共通した前処理や後処理を記述できます。Spanner Emulatorを用いる上でパッケージ内で共通した処理はInstanceを作成することなので、図のようにTestMainにおいてInstanceを作成し、各テスト関数は同一のInstance上にデータベースを作成します。
ここで注意点として、インスタンスが既に存在する場合は、エラーハンドリングをスキップしていることです。ローカルで実行する場合は、Spanner Emulatorのコンテナを常時起動する形になるため、テストを複数回実行したときに同じインスタンスを使い回すようにしています。
作成したインスタンスにデータベースを作成するとき、インスタンス内に同一名のデータベースを作成できないため、各テスト関数ではそれぞれ固有名でデータベースを作成しなければいけません。データベース作成は各テスト関数ごとで行われ、さらにGoの同一パッケージ内ではテスト関数名が重複することはないので、テスト関数名をSuffixとしてデータベース名とします。
テスト関数の冒頭では、データベース作成と同時にテストに必要なテーブルを作成します。ここでは、事前にテーブルのスキーマファイルを用意しておき、データベース作成時にスキーマファイルを読み込んでテーブルを作成しています。
Goで複数のテストケースをテストする際、Table Drivenなテストがよく用いられます。この時、(*testing.T).Run
を用いてサブテストを実装し、サブテスト内で各テストケースを検証します。サンプルコードでも、Table Drivenな形でテストを実装しています。
ここで注意点として、サブテストは並列化しないようにしています。しばしば、(*testing.T).Parallel
を用いることでテストを並列で実行し、テスト実行速度を高速化させることがあります。しかし、サンプルコードではテスト関数冒頭で作成したテーブルをサブテスト間で使い回す形になっているので、サブテスト間でデータ競合が起こってしまう可能性があります。そのため、サブテスト終了と同時にテーブル内のデータをリセットする必要があります。ここでは(*testing.T).Cleanup
を用いることで、呼び出し元のテスト(サブテストを含む)に対する後処理を実装できます。
補足:Spannerでは1個のインスタンスにつき100個のデータベースまでしか作成できないため、サブテスト毎にデータベースを作成するような設計は非現実的です(参照:Spannerのドキュメント)。
テスト関数内のサブテストが終了した後、そのテスト関数内で使用していたデータベースを消去します。これも、事前に(*testing.T).Cleanup
を用いて後処理を登録しておくことで、実装が可能です。他にも、Spanner Clientを閉じる処理について後処理に加えておきます。
Spanner EmulatorをCIで利用する
ここでは、GitHub Actionsで実行する例を紹介します。GitHub Actionsでは、Service Containerという機能を用いることで、CI上でも簡単にSpanner Emulatorを構築できます。
Service Containerは、DockerHubやGoogle Cloud Registryなどにアップロードされているイメージからコンテナを起動することができ、同じjob内であればどのstepからでもアクセスできるため、非常に簡単に利用することが可能です。
おわりに
本記事では、GoのテストでSpanner Emulatorを活用する例をご紹介しました。ここでは、簡単なテストケースをもとにテストコードを実装しましたが、より複雑なシステムなどでは違った構成でテストコードを実装した方が良い、ということも十分あり得るでしょう。
また、GCPにおいては、Spanner Emulatorに限らずFirestore EmulatorやPubSub Emulator (beta)など、多くのEmulatorが利用可能となっています(利用可能なEmulatorはgcloud emulators –help
もしくはgcloud beta emulators –help
で確認できます)。興味のある方は、ぜひ複数のEmulatorを使ったテスト環境の構築方法などを考えてみてください。