YSNHatenaBlog

主にアプリやWebサービス開発について

GitHub ActionsでFirestore Emulatorを使った自動テストを行うためにやったこと

いろいろ苦戦したけど何とかできた。

やりたかったこととしては、Firestore EmulatorをGitHub Actions実行時のCIサーバで起動してCloud Functionsとfirestore.rulesのテストを行うこと。 まだrulesのテストはできてないが、雛形はできたのであとは流れでできそう。

ローカルでテストを導入する

実際の差分はこんな感じ。 github.com

ポイントになる部分について追記。

firebase emulators:exec を使う

package.jsonにtestのscriptを追加するが、こんな感じにした。

  "scripts": {
    "test": "firebase emulators:exec 'jest --silent'"
  },

firebase emulators:exec コマンドは便利で、その後のコマンド実行前にFirebase Emulatorのダウンロード、起動、コマンド実行終了時のFirestore Emulatorの停止をやってくれる。

jest --silent にしたのは、テスト実行時にCloud Functionsのログが吐かれてテスト結果が見にくくなるため。

Cloud Functionsのindex.tsを分割

firebase-functions-test でラップするためにCloud Functionsの実装がexportされたファイルをimportする必要があるが、最初全部のCloud Functionが入ったindex.tsをimportしようとして、nextもロードいろいろ設定が足りなくてエラーになってしまう。そこでnextとそれ以外のCloud Functionを分けて、next以外のCloud Functionだけimportするようにした。

src/functions
├── index.ts
├── tsconfig.json
├── __tests__
├── firestore ← next以外(Firestore Trigger)のCloud Function
└── web ←nextのCloud Function

algoliaのモック

ハマったけどこんな感じ。

let mockSaveObject: ReturnType<typeof jest.fn>
jest.mock('algoliasearch', () => {
  mockSaveObject = jest.fn().mockReturnValue({})
  return {
    default: () => ({
      initIndex: () => ({
        saveObject: mockSaveObject,
      }),
    }),
  }
})

GitHub Actionsの導入

実際の差分はこんな感じ。 github.com

敢えて公開レポジトリでやっているが故にいろいろハマった。APIキーなどが入っている設定ファイルはすべてローカル(開発者のマシン上)にしかない状態にし、GitHub ActionsではそのファイルがなくてもFirestore Emulatorを起動させてテストが実行できるようにする必要があった。

結論としては設定ファイルの値がロードできなかった場合は適当な文字列をデフォルトでつけるようにした。

workflow設定

環境は現時点でのCloud Functionsの実行環境に合わせた。 初めてGitHub Actionsを使ったが、Dockerfileを書かなくてもこれでいい感じやってくれるのが嬉しい。

エミュレータを起動するプロジェクトを指定する必要があり、かつ指定するためにFirebaseの権限が必要になるため GCLOUD_PROJECTFIREBASE_TOKEN を「GitHub > Settings > Secrets」から指定した。

name: CI
on: [push]
jobs:
  test:
    runs-on: ubuntu-18.04
    env:
      FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
      GCLOUD_PROJECT: ${{ secrets.GCLOUD_PROJECT }}
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v1
        with:
          node-version: '10.18.1'
      - uses: actions/setup-java@v1
        with:
          java-version: '15.0.2'
          java-package: jdk
          architecture: x64
      - name: Install dependencies
        run: yarn
      - name: Build
        run: yarn preserve
      - name: Test
        run: yarn test --project ${GCLOUD_PROJECT}

この設定にする前に調べてみたが、GitHub ActionsでFirestore Emulatorを使うのにDocker Composeを使う例が多い。 - docker-compose × Firebase Emulatorでローカル環境構築 - GitHub Actions上でFirestoreエミュレータを立ち上げてテストする - Qiita

やれることが違いそうだけど今回はシンプルにCloud Functionとfirestore.rulesのテストをしたいだけなので、firebase emulators:exec を使って特に複数コンテナは起動せずに実行することにした。設定がより複雑になりそうだったし。

functions.config()のところ

.runtimeconfigがあれば値を渡せるがpushしたくないので、こんな感じでデフォルト値を指定して、エラーにならないようにしておく。 この値を受け取る関数などは、当然本来あるはずの値が渡ってこないので、テスト時はモックする。

functions.config().algolia?.app_id ?? ''

フロントエンドで使うパラメータのところ

いままで config.ts に書くようにしてgitignoreでpushしないようにしていたが、それだとCI側でビルドが通らなくなってしまう。 結論としてはnextで .env が使えるのでそれを読み込むようにし、 .envがなくて設定値が取れないときはデフォルトの値を読むようにした。

nextjs.org

.env場所はsrc/appの下。クライアントサイドでも読み込む可能性のある値は NEXT_PUBLIC_ をprefixに付ける必要がある。

NEXT_PUBLIC_FIREBASE_API_KEY=xxx

最初JSONファイルを作って読み込ませようと思ったが、Dynamic Importもrequireもビルド時にそのファイルが無いとエラーになってしまうことを知った。 また firebase.initializeApp に渡す値は空文字だとエラーになってしまう。

結果

f:id:yosuke403:20210305060628p:plain

まとまった時間がとれず随分時間がかかった。まだrulesのテストが無いので追加したい。

ビルドいらなかった

ここまで書いてふと「別にテストであれば実際にFirestore TriggerのCloud Functionが発火しなくてもいいわけで、firestoreのエミュレータだけ起動すれば、Cloud Functionsのビルドってしなくていいはずだよな...」と気づいてしまった。

github.com

やっぱりこれでよかった。無駄な苦労したかも。

まぁフロントエンドのテストもこれから書いてみたいし、.envは環境ごとに切り替えたりもできそうで便利だから、まぁいいか...。