Firebase Local Emulator SuiteをDocker環境で構築して、アプリケーションからテストを実行してみた

# はじめに

Firebaseをローカルでテストできる[Firebase Local Emulator Suite](https://firebase.google.com/docs/emulator-suite?hl=ja)をDocker環境で構築、アプリケーション(Go)から使用する機会がありました。
本記事では、Dockerでの構築方法及び、アプリケーション側の設定について記載していきます。
※今回は、Firestoreでの利用となります。

# 環境
- macOS Monterey 12.4
- docker desktop 4.7.0
- Go 1.18.3

# 構築
プロジェクト構成は以下の前提で進めていきます。
firebase設定ファイルすでにある事を想定し、実際の環境とも併用することを考慮して、使いまわします。
```
root/
　├ docker/
　│　├ firebase/
　│　│　　├ bin/
　│　│　　├ config/
　│　└ └ Dockerfile
　├ src/
　├ .firebaserc
　├ firebase.json
　├ firestore.indexes.json
　├ firestore.rules
　└ docker-compose.yml
```

エミュレータを使用するために、`firebase.json`に以下の設定を追加（ポート番号は状況に応じて変更）
```firebase.json
  "emulators": {
    "auth": {
      "host": "0.0.0.0",
      "port": 9099
    },
    "functions": {
      "host": "0.0.0.0",
      "port": 5001
    },
    "firestore": {
      "host": "0.0.0.0",
      "port": 8080
    },
    "database": {
      "host": "0.0.0.0",
      "port": 9000
    },
    "hosting": {
      "host": "0.0.0.0",
      "port": 5050
    },
    "pubsub": {
      "host": "0.0.0.0",
      "port": 8085
    },
    "storage": {
      "host": "0.0.0.0",
      "port": 9199
    },
    "ui": {
      "enabled": true,
      "host": "0.0.0.0",
      "port": 4000
    }
  }
```

## Dockerの設定
Emulatorを使用するために、
- Node.js バージョン 8.0 以降。
- Java JDK バージョン 11 以降。
- Firebase CLI
が必要なため、各種インストール。

```Dockerfile
FROM ubuntu:22.04

RUN apt update && \
apt install -y curl openjdk-17-jdk && \
curl -fsSL https://deb.nodesource.com/setup_16.x | bash - && \
apt install -y nodejs

RUN npm install -g firebase-tools
```


firebaseを使用するためのアプリケーション及び上記のDockerfileを指定したcomposeファイルを作成します。
ポートは同様のものを指定し、docker立ち上げ時に自動でエミュレータが起動するように`firebase emulators:start`を指定しておきます。

```docker-compose.yml
version: '3.9'

services:
  app:
    build:
      context: .
      dockerfile: ./docker/app/Dockerfile
    env_file: .env
    ports:
      - "3000:8080"
    volumes:
      - ./src/:/go/src
    tty: true
  firebase:
    build:
      context: .
      dockerfile: ./docker/firebase/Dockerfile
    volumes:
      - ./.firebaserc:/opt/firebase/.firebaserc
      - ./firebase.json:/opt/firebase/firebase.json
      - ./firestore.indexes.json:/opt/firebase/firestore.indexes.json
      - ./firestore.rules:/opt/firebase/firestore.rules
      - ./docker/firebase/bin/:/root/.cache:cached
      - ./docker/firebase/config/:/root/.config:cached
    ports:
      - 9099:9099 # Firebase Authentication
      - 5001:5001 # Clound Functions
      - 8080:8080 # Cloud Firestore
      - 9000:9000 # Realtime Database
      - 5050:5050 # Firebase Hosting
      - 8085:8085 # Cloud Pub/Sub
      - 9199:9199 # Cloud Storage
      - 4000:4000 # Emulator Suite UI
    working_dir: /opt/firebase
    command: firebase emulators:start
    tty: true
```

## 初期設定
初回実行時は以下の初期設定を行って、firebaseの設定を行う。
1. firebaseログイン
1. firebase初期化

### 1.firebaseログイン
`docker compose run --rm firebase firebase login --no-localhost`や
Docker内で、`firebase login --no-localhost`を実行してログインする。
（実際のfirebaseプロジェクトが作成済であることが前提、権限があるアカウントにてログイン）

### 2.firebase初期化
`docker compose run --rm firebase firebase init`や
Docker内で、`firebase init`を実行して初期化する。
対話形式で、設定を聞かれるので、**Emulator**を選択、port番号は設定済みで、最後にエミュレータをダウンロードするか聞かれるので、インストール。
binフォルダ配下にエミュレータがインストールされる

## 起動
初期設定後、`docker compose up`等でコンテナ起動するとlocalhost:4000でエミュレータ画面にアクセスできるようになります。
![localhost_4000_ (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/457889/8002c157-9209-84f7-5e67-3707be5f9f5c.png)

# アプリケーションから接続
エミュレータが起動したので、簡単なテストを実装しました。

[Firebase Admin SDK](https://firebase.google.com/docs/firestore/quickstart?hl=ja#go)を利用していますので、詳細はそちらをご覧いただければと思います。

ポイントとしては以下２点
- エミュレータに[データクリアの機能](https://firebase.google.com/docs/emulator-suite/connect_firestore?hl=ja#clear_your_database_between_tests)が備わっているため、テストごとTearDownを行うことで、クリーンな状態で各種テストが実行が可能
- 環境変数`FIRESTORE_EMULATOR_HOST`の設定有無によって、エミュレータ接続するかどうかが決まるため、プロダクションコードに大きな変更を加えることなく、テスト可能

```sample.go
import (
	"context"
	"fmt"
	"os"

	"cloud.google.com/go/firestore"
	"google.golang.org/api/option"
)

func getFirestore(ctx context.Context) (*firestore.Client, error) {
	sa := option.WithCredentialsFile(os.Getenv("GOOGLE_APPLICATION_CREDENTIALS"))
	projectId := os.Getenv("PROJECT_ID")
	client, err := firestore.NewClient(ctx, projectId, sa)
	if err != nil {
		fmt.Println(err)
	}
	return client, err
}

func GetDocument(c, docId string) (*firestore.DocumentSnapshot, error) {
	ctx := context.Background()
	client, err := getFirestore(ctx)

	// コレクション検索
	snapshot, err := client.Collection(c).Doc(docId).Get(ctx)
	if err != nil {
		fmt.Println(err)
	}
	return snapshot, err
}

func UpsertDocument(c, docId string, data interface{}) error {
	ctx := context.Background()
	client, err := getFirestore(ctx)
	if err != nil {
		return err
	}

	_, err = client.Collection(c).Doc(docId).Set(ctx, data)
	if err != nil {
		return err
	}

	return nil
}

```

```sample_test.go
import (
	"fmt"
	"net/http"
	"os"
	"testing"

	"github.com/mobility-api/middleware"
	"github.com/mobility-api/model"
	"github.com/stretchr/testify/suite"
)

type Companies struct {
	suite.Suite
}

func (suite *Companies) SetupTest() {
	//初期データ投入
	for i, v := range []string{"sample1", "sample2", "sample3"} {
		i++
		middleware.UpsertDocument("companies", v, map[string]interface{}{"name": fmt.Sprintf("name%d", i)})
	}
}

func (suite *Companies) TearDownTest() {
	//全データ削除
	url := fmt.Sprintf("http://%s/emulator/v1/projects/%s/databases/(default)/documents", os.Getenv("FIRESTORE_EMULATOR_HOST"), os.Getenv("PROJECT_ID"))
	req, _ := http.NewRequest(http.MethodDelete, url, nil)
	client := http.Client{}
	client.Do(req)
}

func TestCompanies(t *testing.T) {
	os.Setenv("FIRESTORE_EMULATOR_HOST", "localhost:8080")
	os.Setenv("PROJECT_ID", "sample-project")
	suite.Run(t, &Companies{})
}

func (suite *Companies) TestCompanies_GetCompany() {
	docID := "sample1"
	actual, err := middleware.GetDocument("companies", docID)
	suite.Nil(err)
	suite.Equal(&model.Company{
		DocId: docID,
		Name:  "name1",
	}, actual)
}

```

# まとめ
Firebaseエミュレータを使用して、起動・アプリケーションからのテストまで作成しました。
Firestore以外にも各種エミュレータを使用できるため、ローカルでの動作確認には、非常に役に立つのではないかと思いました。