GitHub Actions上でFirestoreエミュレータを立ち上げてテストする

エミュレータを使ってCircle CI上でテストされている方はいらっしゃるのですが、GitHub Actionsでの情報は見かけなかったのでまとめてみます。

大まかな流れは以下の通りです。

* firestoreエミュレータ入りのDockerfileを書く
* firestoreエミュレータを立ち上げるdocker-compose.ymlを書く
* GitHub Actionsのワークフロー上でdocker-composeを実行してテスト

なお、エミュレータはJavaで実装されているので、Javaの実行環境が無ければあらかじめインストールしておいてください。

## セットアップ

まずは`firebase init`でFirestoreをプロジェクトにセットアップします。コマンドを実行するとどれをセットアップするかを聞かれるので、Firestoreを選択します。その後、セキュリティルールやインデックスファイルの名前を指定できますが、とりあえずエンター連打でデフォルトのまま進みます。

```bash
$ firebase init
(略)
? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices. 
Firestore: Deploy rules and create indexes for Firestore

=== Project Setup
(略)
? Please select an option: Use an existing project(ここでは既存のプロジェクトを選択していますが、新しく作っても以降の操作は同じです。)
(略)
=== Firestore Setup
(略)
? What file should be used for Firestore Rules? firestore.rules
(略)
? What file should be used for Firestore indexes? firestore.indexes.json

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...
i  Writing gitignore file to .gitignore...

✔  Firebase initialization complete!

```

これで、プロジェクトに以下のファイルが生成されているはずです。

* firebase.json
* firestore.index.json
* firestore.rules

次にエミュレータをセットアップします。今回は説明のために先ほどのFirestoreセットアップと手順を分けましたが、実際はfirebase initで同時に行っても大丈夫です。

firebase initで`Emulators`を選択すると、どのエミュレータをセットアップするか聞かれるので、Firestoreを選択します。あとは先ほどと同じように連打でOKですが、`Would you like to download the emulators now?`では`Y`を選択してください。そうすると、エミュレータのjarが落ちてきます。

```bash
$ firebase init
(略)
? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices. 
Emulators: Set up local emulators for Firebase features
(略)
=== Emulators Setup
? Which Firebase emulators do you want to set up? Press Space to select emulators, then Enter to confirm your choices. (Press <space> to select, <a> to toggle all, <i> to invert selection)
Firestore

? Which port do you want to use for the firestore emulator? 8080
? Would you like to download the emulators now? Yes

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...

✔  Firebase initialization complete!
```

`firebase.json`にエミュレータの項目が追加されているはずです。

```diff
   "firestore": {
     "rules": "firestore.rules",
     "indexes": "firestore.indexes.json"
+  },
+  "emulators": {
+    "firestore": {
+      "port": 8080
+    }
   }
 }
```

ちゃんと動くか試してみましょう。以下のコマンドでエミュレータが起動します。

```bash
$ firebase emulators:start --only firestore
i  emulators: Starting emulators: firestore
i  firestore: Serving ALL traffic (including WebChannel) on http://localhost:8080
⚠  firestore: Support for WebChannel on a separate port (8081) is DEPRECATED and will go away soon. Please use port above instead.
i  firestore: Emulator logging to firestore-debug.log
✔  firestore: Emulator started at http://localhost:8080
i  firestore: For testing set FIRESTORE_EMULATOR_HOST=localhost:8080
✔  All emulators started, it is now safe to connect.
```

portを`8080`に設定したので、`localhost:8080`をlistenしています。HTTPリクエストを投げると、動いているかどうかが確認できます。

```bash
$ curl http:localhost:8080
Ok
```

## セキュリティルールとテストを書いてみる

エミュレータが準備できたので、試しにセキュリティルールとそのテストを書いてみましょう。何かしらのプロジェクト一覧のうち、自分がオーナーであるプロジェクトだけが読み書きできるルールは以下のようになります。

```
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /projects/{projectID} {
      allow read, write: if resource.data.ownerId == request.auth.uid;
    }   
  }
}
```

このセキュリティルールに対するテストは以下のようになります。[Cloud FirestoreのSecurity RulesをCircleCIで自動テストする \- ninjinkun's diary](https://ninjinkun.hatenablog.com/entry/cloud-firestore-circleci)を参考にJestで書いてみました。

```tsx
import * as firebase from '@firebase/testing';

describe('/projects', () => {
    const uid = 'alice';
    const db = firebase.initializeTestApp({
        auth: { uid, email: 'alice@example.com' },
        projectId: 'my-test-project'
      }).firestore();
    const projectCollection = db.collection('projects');

  it('can not list all projects', async () => {
    await firebase.assertFails(projectCollection.get());
  });
  
  it('can list own projects', async () => {
    await firebase.assertSucceeds(
      projectCollection.where('ownerId', '==', uid).get()
    );
  });
});
```



## Dockerの設定

ここまでで、ローカルでエミュレータが動くのを確認しました。次にこのエミュレータをDockerコンテナ上で動くようにします。[firestore local emulator を docker\-composeで動かす - Qiita](https://qiita.com/hiko1129/items/dd0321f6d1747838b577)を参考にしました。

```dockerfile
FROM node:10-alpine
WORKDIR /usr/src/app
RUN apk add --no-cache openjdk8-jre
RUN npm i -g firebase-tools && firebase setup:emulators:firestore
```

先ほどは`firebase init`でエミュレータを設定しましたが、Dockerfileでは`setup:emulators`コマンドを利用しています。多分どちらでも同じだと思います。

docker-compose.ymlも書きます。セキュリティルールを読み込みたいので、カレントディレクトリをマウントしておきましょう。

```yaml
version: '2'
services:
  firestore:
    build: .
    ports:
      - '8080:8080'
    volumes:
      - .:/usr/src/app
    working_dir: /usr/src/app
    command: 'firebase emulators:start --only firestore'
```

もしホスト側のポートを8080以外にする場合は、テスト実行側でFIRESTORE_EMULATOR_HOST環境変数を設定してください。

注意点として、エミュレータはデフォルトではlocalhostからのリクエストからしか受け付けないため、コンテナ外からのアクセスがそのままだとできません。そこで、`firebase.json`を以下のように書き換えます。

```diff
   "emulators": {
     "firestore": {
+      "host": "0.0.0.0",
       "port": 8080
     }
   }
```

これでコンテナ外からもアクセスできるようになります。

`docker-compose up`で、コンテナ上でエミュレータが起動します。

```bash
$ docker-compose up firestore
Starting firebase-todoapp_firestore_1 ... done
Attaching to firebase-todoapp_firestore_1
firestore_1  | i  emulators: Starting emulators: firestore
firestore_1  | i  firestore: Serving ALL traffic (including WebChannel) on http://0.0.0.0:8080
firestore_1  | ⚠  firestore: Support for WebChannel on a separate port (8081) is DEPRECATED and will go away soon. Please use port above instead.
firestore_1  | i  firestore: Emulator logging to firestore-debug.log
firestore_1  | ✔  firestore: Emulator started at http://0.0.0.0:8080
firestore_1  | i  firestore: For testing set FIRESTORE_EMULATOR_HOST=0.0.0.0:8080
firestore_1  | ✔  All emulators started, it is now safe to connect.
```

## GitHub Actions Workflowの設定

最後にWorkflowを設定します。オフィシャルのNodejsワークフローを参考に、以下のように設定しました。

```yml
name: Firebase CI

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [12.x]

    steps:
      - uses: actions/checkout@v1
      - name: docker-compose up
        run: docker-compose up -d
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
      - name: yarn install, build, and test
        run: |
          yarn install
          yarn tslint
          yarn test
          yarn build
        env:
          CI: true
```

`docker-compose up`でエミュレータが起動され、`yarn test`で先ほど書いたセキュリティルールを含むテストが実行されます。テスト実行開始までにエミュレータが上がっている保証はありませんが、[惑星よりも重いことで有名な](https://www.reddit.com/r/ProgrammerHumor/comments/66cofz/heavy_objects/)`yarn install`を実行している間にまず間違いなく上がるので良しとしましょう。気になる方は定期的にcurlを叩いてレスポンスが返ってくるまで待つシェルスクリプトを書いたりすると良いと思います。

これでGitHub Actions上でFirestoreエミュレータを立ち上げてテストができるようになりました。実行時間は(Dockerfileのビルドからやっているので)docker-compose upが1分、yarn installが3分ぐらいです。
キャッシュをちゃんとやればもっと早くなるはずなので、調べたらまた記事を書きたいと思います。