Firebase EmulatorでCloudFunctionsとFirestoreを起動し、APIテストを実行する方法

4 min読了の目安(約4400字TECH技術記事

はじめに

firestoreへの処理が正しいかテストをする際、実際のプロジェクトではなく、ローカルの閉じた環境内で行いたいことがあると思います。
そこで今回、CloudFunctionsとFirestoreをemulatorで立ち上げ、jestでAPIを投げリクエストとレスポンスが正しいかどうかのテストを行いました。

方法

CloudFunctions

CloudFunctionsでfirestoreへのCRUD処理を1つずつ作成しています。
テストではCUDのエンドポイントを叩き、Rのエンドポイントでレスポンス値が想定通りであるかの確認をしています。

index.ts
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
admin.initializeApp();

const db = admin.firestore();

export const createDocument = functions.https.onRequest(async (req, res) => {
  await db.collection("test").doc("abc").set({a: 123});
  res.send();
});

export const updateDocument = functions.https.onRequest(async (req, res) => {
  await db.collection("test").doc("abc").update({a: 234, b: 345});
  res.send();
});

export const deleteDocument = functions.https.onRequest(async (req, res) => {
  await db.collection("test").doc("abc").delete();
  res.send();
});

export const getDocument = functions.https.onRequest(async (req, res) => {
  const data = await (await db.collection("test").doc("abc").get()).data();
  res.send(data);
});

emulatorの立ち上げ

設定は公式ドキュメントを見るのが一番わかりやすいです。

https://firebase.google.com/docs/emulator-suite/install_and_configure?hl=ja

自分が試した時の設定は下記です。

firebase.json
{
  "firestore": {
    "rules": "firestore.rules",
    "indexes": "firestore.indexes.json"
  },
  "functions": {
    "predeploy": [
      "npm --prefix \"$RESOURCE_DIR\" run lint",
      "npm --prefix \"$RESOURCE_DIR\" run build"
    ]
  },
  "storage": {
    "rules": "storage.rules"
  },
  "emulators": {
    "auth": {
      "port": 9099,
      "host": "localhost"
    },
    "functions": {
      "port": 5001,
      "host": "localhost"
    },
    "firestore": {
      "port": 8089,
      "host": "localhost"
    },
    "ui": {
      "enabled": true
    }
  },
  "remoteconfig": {
    "template": "remoteconfig.template.json"
  }
}

設定ファイルを作成したら、 firebase emulators:start を実行することでemulatorsが立ち上がります。
ui.enabledtrue にしているとFirebase Emulator Suiteを確認することができます。
こちらで今起動しているサービスを確認できます。また、こちらのコンソールから手動でデータ作成や削除なども行えます。

tips

emulatorsは終了するとデータは削除され、再度起動した際は空の状態になります。
ただ、起動時に初期データを入れておきたい。また、再起動してもデータを使い回したいということもあると思います。
emulatorsはそういう時のオプションを用意しています。--import--export-on-exit を使用することで永続化可能です。
firesoreの場合は、実際のプロジェクトからデータをエクスポートして、emulatorにインポートすることも可能です。

https://firebase.google.com/docs/firestore/manage-data/export-import?hl=ja

テストの実行

emulatorsで起動したCloudFunctionsはデフォルトで次のようなエンドポイントで実行可能です。

https://localhost:$ポート番号/$PROJECT/us-central1/$関数名

https://firebase.google.com/docs/emulator-suite/connect_functions?hl=ja#instrument_your_app_for_https_functions_emulation

そのため、jestでは単純にAPIを投げて動作確認することができます。

api.test.ts
import axios from 'axios'

describe("test", () => {
  describe('createDocument APIを実行のテスト', () => {
    beforeAll(async() => {
      await axios.post('http://localhost:5001/firebase-tutorial-suzuki/us-central1/createDocument');
    })
    test('getDocumentで想定通りのレスポンスが返ってくる', async() => {
      const response = await axios.get('http://localhost:5001/firebase-tutorial-suzuki/us-central1/getDocument');
      expect(response.data).toEqual({a: 123})
    })
  })

  describe('updateDocument APIを実行のテスト', () => {
    beforeAll(async() => {
      await axios.post('http://localhost:5001/firebase-tutorial-suzuki/us-central1/updateDocument');
    })
    test('getDocumentで想定通りのレスポンスが返ってくる', async() => {
      const response = await axios.get('http://localhost:5001/firebase-tutorial-suzuki/us-central1/getDocument');
      expect(response.data).toEqual({a: 234, b: 345})
    })
  })

  describe('deleteDocument APIを実行のテスト', () => {
    beforeAll(async() => {
      await axios.post('http://localhost:5001/firebase-tutorial-suzuki/us-central1/deleteDocument');
    })
    test('getDocumentで想定通りのレスポンスが返ってくる', async() => {
      const response = await axios.get('http://localhost:5001/firebase-tutorial-suzuki/us-central1/getDocument');
      expect(response.data).toEqual("")
    })
  })
});

おわりに

emulatorを使ったテストは以外と簡単でした。API界面でin/outのテストもあれば、リファクタリングするときも安心できそうです。
今回はcloudfunctionsとfirestoreだけでしたが、Authenticationを絡めた認証周りも調査してみたいです。
emulatoreを動かすにはJavaが必要なので、CI上で乗せる所も確認したいです。

また、Cloud Functions は単体テストで firebase-functions-test というものもあるようです。

https://firebase.google.com/docs/functions/unit-testing?hl=ja

こちらも時間を見つけて確認してみたいと思います。
(やってもやっても、分からないことが増え、やりたいことも増えますね)