YSNHatenaBlog

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

Firestoreの住所のfieldを整理

いままではプロトタイプの意味も含めて住所は1 fieldに格納していたけど、都道府県、市区町村、残りで分けるようにしてみた。もっと細かくもできたかもしれないけど、どうやって探すかを考えればそこまで詳細じゃなくてもよいでしょうと。入力も大変だし。

いったん最低限のことはできたと思うので、ここからしばらくはデータを入れていこうと思う。

github.com

Firestoreのルールのテストを書いた

Firestore rulesのテストを書いていく。

initializeAdminAppで取得したappでfirestoreに書き込む際、Timestampの型はfirebase-admin由来でないとエラーになった。

Value for argument "data" is not a valid Firestore document. Detected an object of type "Timestamp" that doesn't match the expected instance (found in field "createdAt"). Please ensure that the Firestore types you are using are from the same NPM package.)

Timestampを@firebase/rules-unit-testingではなくfirebase-adminから型を取ったら直った。

次のエラー。

(node:18029) UnhandledPromiseRejectionWarning: Error: FIRESTORE (8.2.5) INTERNAL ASSERTION FAILED: Unexpected state
(node:18029) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 33)

stackoverflow.com

jest --env=node

にする。

また、作成したapp(adminもそうでないのも)は都度delete()で消さないとCI上だとjestが終了しなくなる。 jest --forceExit を使うと終了させることができるが、気持ち悪いなら都度掃除する。

テストコードの最初はこんな感じになった。

import * as firebase from '@firebase/rules-unit-testing'
import * as admin from 'firebase-admin'

const projectId = 'rules-courts-test'

beforeEach(async () => {
    firebase.loadFirestoreRules({
        projectId,
        rules: fs.readFileSync('firestore.rules', 'utf8'),
    })

    const adminApp = firebase.initializeAdminApp({ projectId })
    // adminAppを使ってテストの初期状態となるデータを書き込んでいく
    await adminApp.delete()
})

afterEach(async () => {
    await firebase.clearFirestoreData({ projectId })

})

また、公式ガイドだとassertSucceeds, assertFailsはawait無しで書いてあるので、もしやいらない?と思ったが、awaitしないと正しいテスト結果にならなかった。

test('read shoud be succeeded', async () => {
    await firebase.assertSucceeds(
        app.firestore().collection('courts').doc('Slsnk6XjulO3ndipFjlY').get()
    )
})

Youtubeでもawait入れてるしよさそう。

www.youtube.com

実際の差分はこうなった。

github.com

Firestoreルールのテストの書き方を再調査

Firestoreルールのテストの書き方を復習がてら再調査。

firebase.google.com

カバレッジの取得。そういえばできた気がする。

http://localhost:8080/emulator/v1/projects/<project_id>:ruleCoverage.html

Firestoreに限らないかもしれないが、最初のデータセットアップが結構めんどくさい。 本家にもAdminAppを使うぐらいしか解を書いていない。

テストのセットアップが非常に複雑である

firebase.google.com

予め作った、データセットが定義されたファイルからインポートできたらいいのになぁと思っているのだが...

ちなみにテスト前にデータのimportはできるかもしれない。が、beforeEachで都度インポート、みたいなことはできなそう。 firebase.google.com

いろいろ調べたが、やはり都度AdminAppでやるしかなさそう。

基本的には*.test.tsごとにプロジェクトIDを指定し、beforeEachでデータセットアップ、afterEachでclearFirestoreDataするようにしたい。 beforeEachごとにプロジェクトのIDを更新するというのが前あったが、使い捨てプロジェクトがガンガン生成されるためちょっときれいじゃない感がある。他に方法なさそうならこれでもいいけど。

目的と直接関係ないが@firebase/rules-unit-testingにこんなのがあった。

withFunctionTriggersDisabled

github.com

この引数のクロージャの中でFirestoreを書き換える分にはFirestore Triggerが発火しないらしい。でも実装見てる感じ、クロージャの中実行前後でエミュレータのWeb API叩いて全体でdisableさせてるみたいなので、並列でテストが実行されていたりするとおかしなことになりそう。

Firebase Emulator x Next.jsでnet::ERR_CONTENT_DECODING_FAILED

Firebase Emulator x Next.jsの構成で、yarn serveを使うとブラウザで net::ERR_CONTENT_DECODING_FAILED が出ていた。

net::ERR_CONTENT_DECODING_FAILED 200 (OK)

gzipされてないのにgzipのヘッダがついてる模様。 github.com

結局このissueからたどって直ってるっぽいことが分かり、 github.com

firebase-toolsを9.2.0から9.6.0にアップデートしただけで直った...。

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は環境ごとに切り替えたりもできそうで便利だから、まぁいいか...。

firebaseの設定ファイルをpushしたくない

テストはfirebaseの設定ファイルいらないはずなので、pushしなくてもGitHub Actionsを回せるのだが、手元の開発環境では設定ファイル参照して、GitHub Actions実行時は設定ファイルを無視あるいはダミーする、みたいなことがいい感じにできないか考え中。

一応他の開発者の参考になればと思い、公開レポジトリにしているのでこの悩み。

github.com

結局ユーザには見える値なのでどうしても隠さなければいけない情報ではないのだけど、レポジトリに含まれてしまうのは気持ち悪いなと。

この辺の環境変数が使えそうだなぁということろまで。

nextjs.org

GitHub ActionsでFirestore Emulatorを使ったCIをやりたい

この辺が参考になりそう。 qiita.com

調べてみるとDocker Composeでやってる例が多そう。

これも参考になりそう。 zenn.dev

import, exportできるのよい。テスト用のデータセット最初に作ってしまうのがよさそう。 importの際にCloud Functionsは発火しないのかな?