【Go】Firebase Emulatorとdockertestでテストを書く
概要
本記事では、Firebase Emulatorとdockertestを使用して、Firebaseを使ったGoプログラムをテストする方法を紹介します。
対象読者
- GoでFirebaseを使ったテストを簡単に書きたい方
- Firebaseを使ったテストをCI/CDで自動化したい方
OS、言語、ツールのバージョン
- MacOS Sonoma(14.2)
- Docker(24.0.6)
- Go(1.22.3)
- github.com/ory/dockertest/v3(3.10.0)
- cloud.google.com/go/firestore(1.15.0)
今回使用するツールの説明
Firebase Emulatorとは
上記画像の出典: Firebase ローカル エミュレータ スイートの概要 | Firebase Local Emulator Suite
Firebase Emulatorは、Firebaseの機能をローカル環境でテストするためのツールです。
対応しているFirebaseのサービスは以下の通りです。
- Cloud Firestore
- Realtime Database
- Cloud Storage for Firebase
- Authentication
- Firebase Hosting
- Cloud Functions(ベータ版)
- Pub/Sub(ベータ版)
- Firebase Extensions(ベータ版)
上記のサービスに加えて、Firebase Emulator UIが用意されており、本番環境と同じようにWeb画面からデータを直接編集することが可能です。
公式ドキュメントに詳細な説明が載っているので、気になる方はぜひ読んでみてください。
dockertestとは
dockertestは、GoでDockerコンテナを起動できるパッケージです。
処理が完了したらコンテナを削除するなど、テストに便利な機能が揃っています。
詳細はGitHubのREADMEを参照してください。
ツールの動作に必要なファイルを作成する
テストコードを書く前に、Firebase EmulatorとDockerを動かすために必要な3つのファイルを作成しましょう。
- .firebaserc
- firebase.json
- Dockerfile
Firebase Emulatorの設定ファイルは、firebase-tools
のコマンドを使って作成することもできますが、今回は記載量が少ないので手動で作成します。
.firebaserc
まず、Firebaseプロジェクトのデフォルト設定を.firebaserc
に記載します。
{
"projects": {
"default": "firebase-emulator-test"
}
}
firebase-emulator-test
の部分は適当なプロジェクトIDに置き換えてください。
今回はローカルで完結するため、どのようなプロジェクトIDでも構いません。
firebase.json
次に、Firebase Emulatorで使用するサービスの設定をfirebase.json
に記載します。
{
"emulators": {
"firestore": {
"host": "0.0.0.0",
"port": 8098
},
"singleProjectMode": true
}
}
host
に0.0.0.0
を指定しないと、ホストPCからDockerで動作しているFirestoreに接続できないので注意しましょう。
Firebase Emulatorは、デフォルトでlocalhost
からのリクエストにのみ応答するため、ホストPCからDockerコンテナには接続できません。
詳細は公式ドキュメントを参照してください。
Dockerfile
最後に、dockertestで使用するDockerfile
を作成します。
FROM node:22.0.0-slim
RUN apt-get -y update && \
apt-get -y --no-install-recommends install default-jdk && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN npm install -g firebase-tools@13.8.3
ENTRYPOINT [ "firebase", "emulators:start" ]
Dockerfile
では、Firebase Emulatorをインストールするために必要なライブラリをインストールします。
- Firebase CLI 8.14.0+
- Node.js 16.0 +
- Java JDK 11 +
実際にテストを書く
ツールを動かすためのファイルが準備できたので、実際にテストを書いていきましょう。
今回は以下のディレクトリ構成で進めていきます。
.
└── firestore/
├── firestore.go
└── firestore_test.go
テスト対象のコード(firestore.go)
まずはテスト対象のサンプルコードを記載します。
Firestoreの会員情報を操作するだけのシンプルな実装です。
package firestore
import (
"context"
"cloud.google.com/go/firestore"
)
const membersCollectionID = "members"
// Client Firestoreクライアント
type Client struct {
client *firestore.Client
ctx context.Context
}
// Member 会員情報
type Member struct {
Name string
}
// NewClient Firestoreクライアントを初期化する
func NewClient() (*Client, error) {
ctx := context.Background()
client, err := firestore.NewClient(ctx, "firebase-emulator-test")
if err != nil {
return nil, err
}
return &Client{client, ctx}, nil
}
// CreateMember 会員情報を作成する
func (c *Client) CreateMember(id string, name string) error {
m := &Member{
Name: name,
}
_, err := c.client.Collection(membersCollectionID).Doc(id).Create(c.ctx, m)
return err
}
// FetchMember 会員情報を取得する
func (c *Client) FetchMember(id string) (*Member, error) {
snap, err := c.client.Collection(membersCollectionID).Doc(id).Get(c.ctx)
if err != nil {
return nil, err
}
m := new(Member)
if err := snap.DataTo(m); err != nil {
return nil, err
}
return m, nil
}
// Close Firestoreクライアントが保持するリソースを開放する
func (c *Client) Close() {
c.client.Close()
}
テスト対象のコード(firestore_test.go)
FetchMember
メソッドのテストコードを記載します。
package firestore_test
import (
"log"
"os"
"testing"
"time"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"github.com/stretchr/testify/assert"
"github.com/wasuwa/firebase-emulator/firestore"
)
const hostEnvKey = "FIRESTORE_EMULATOR_HOST"
var (
client *firestore.Client
host = os.Getenv("FIRESTORE_HOST") + ":8098"
seeds = map[string]*firestore.Member{
"1EA27028-FC44-4388-9999-FC8DD1AEBF47": {"鈴木太郎"},
"5AECA4E6-CBEB-4033-AA4A-0F6DF429D3B7": {"佐藤一郎"},
}
)
func TestMain(m *testing.M) {
resource, err := testMain()
if err != nil {
log.Fatalln(err)
}
defer func() {
os.Unsetenv(hostEnvKey)
client.Close()
resource.Close()
}()
m.Run()
}
func testMain() (*dockertest.Resource, error) {
// カレントディレクトリをプロジェクトのルートに変更する
if err := os.Chdir("../"); err != nil {
return nil, err
}
// Firestoreに接続するために必要な環境変数を設定する
if err := os.Setenv(hostEnvKey, host); err != nil {
return nil, err
}
pool, resource, err := initFirebaseEmulator()
if err != nil {
return nil, err
}
client, err = firestore.NewClient()
if err != nil {
return nil, err
}
if err := insertSeedData(pool); err != nil {
return nil, err
}
return resource, nil
}
func TestFetchMember(t *testing.T) {
tests := []struct {
name string
arg string
want *firestore.Member
expectErr bool
}{
{
name: "Firestoreから会員を取得できること",
arg: "1EA27028-FC44-4388-9999-FC8DD1AEBF47",
want: &firestore.Member{
Name: seeds["1EA27028-FC44-4388-9999-FC8DD1AEBF47"].Name,
},
expectErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := client.FetchMember(tt.arg)
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
}
})
}
}
// initFirebaseEmulator Firebase Local Emulator Suiteを初期化する
func initFirebaseEmulator() (*dockertest.Pool, *dockertest.Resource, error) {
pwd, err := os.Getwd()
if err != nil {
return nil, nil, err
}
pool, err := dockertest.NewPool("")
if err != nil {
return nil, nil, err
}
pool.MaxWait = time.Second * 20
if err := pool.Client.Ping(); err != nil {
return nil, nil, err
}
options := &dockertest.RunOptions{
Name: "firebase-emulator-test",
Mounts: []string{
pwd + "/.firebaserc:/.firebaserc",
pwd + "/firebase.json:/firebase.json",
},
ExposedPorts: []string{
"8098/tcp",
},
PortBindings: map[docker.Port][]docker.PortBinding{
"8098/tcp": {{HostIP: "", HostPort: "8098"}},
},
}
// テスト狩猟後にインスタンスを削除する
resource, err := pool.BuildAndRunWithOptions(pwd+"/Dockerfile", options, deleteInstance)
if err != nil {
return nil, nil, err
}
return pool, resource, nil
}
// deleteInstance インスタンスを削除する
func deleteInstance(config *docker.HostConfig) {
config.AutoRemove = true
config.RestartPolicy = docker.RestartPolicy{
Name: "no",
}
}
// insertSeedData 初期データを投入する
func insertSeedData(p *dockertest.Pool) error {
// Firestoreへの接続が失敗した場合は自動でリトライされる
f := func() error {
for k, v := range seeds {
if err := client.CreateMember(k, v.Name); err != nil {
return err
}
}
return nil
}
return p.Retry(f)
}
FIRESTORE_HOST
は独自に定義した環境変数です。
ローカル実行時はlocalhost
を設定してください。
後述するGitLab CI/CDでテストを動かす場合は、ホストにlocalhost
ではなくdocker
を指定する必要があるので、あえて環境変数で値を管理しています。
テストコードが書けたら、実際にテストを実行してみましょう。
go test -v ./firebase/...
結果を確認すると、無事にテストが成功しています。
> go test -v ./firestore/...
=== RUN TestFetchMember
=== RUN TestFetchMember/Firestoreから会員が取得できること
--- PASS: TestFetchMember (0.01s)
--- PASS: TestFetchMember/Firestoreから会員が取得できること (0.01s)
PASS
ok github.com/wasuwa/firebase-emulator/firestore 108.842s
GitLab CI/CDでテストを実行する
手動でテストを実行するのではなく、自動でテストを実行できると便利です。
.gitlab-ci.yml
を作成し、Gitlab CI/CDでテストを自動実行してみましょう。
test:
image: golang:1.22.3
services:
- docker:dind
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
FIRESTORE_HOST: docker
script:
- go test -v ./...
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
マージリクエストのソースブランチに変更を加えるたびにパイプラインを立ち上げ、テストを実行するように設定しています。
では.gitlab.ci.yml
が完成したので、実際にマージリクエストを作成し、GitLab CI/CDでパイプラインを動かしてみましょう。
...
go: downloading golang.org/x/text v0.14.0
go: downloading github.com/gogo/protobuf v1.3.2
go: downloading github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f
go: downloading golang.org/x/crypto v0.19.0
=== RUN TestFetchMember
=== RUN TestFetchMember/Firestoreから会員が取得できること
--- PASS: TestFetchMember (0.01s)
--- PASS: TestFetchMember/Firestoreから会員が取得できること (0.01s)
PASS
ok github.com/wasuwa/firebase-emulator/firestore 114.863s
Cleaning up project directory and file based variables
00:00
Job succeeded
ジョブが成功しているので、特に問題なさそうです。
Discussion