THUMBS SHIFT→

このブログは主に親指シフトを用いて書かれています

firebaseをどうテストするか

firebaseをいじってて少しだけ知見がたまってきたので共有します。

前提

ローカルでテストしたいなら、Firebase Local Emulator Suite を使いましょう。 yarn run firebase emulators:startで起動できます。

yarn firebase emulators:exec 'yarn test'ってやると、emulator起動してからテストしてくれます。

設定

これを書いておきましょう。ローカルでの起動でemulatorを使うようになります。create-react-appを使ってるのでNODE_ENVで分岐してますが、別に他のでもいいです。あとポートは自分のに合わせましょう。

const app = firebase.initializeApp(firebaseConfig);
export const db = app.firestore();
if (process.env.NODE_ENV !== "production") {
  db.settings({
    host: "localhost:8080",
    ssl: false,
  });
  app.functions().useFunctionsEmulator("http://localhost:5001");
}

firestore

rulesのテストは簡単です。@firebase/rules-unit-testingを使えば下のようなコードですぐテストできます。(@firebase/testingから改名しました)

import {
  assertFails,
  initializeTestApp,
} from "@firebase/rules-unit-testing";

test("denied if not authed", async () => {
    const notAuthedDB = initializeTestApp({ auth: { uid: undefined } }).firestore();
    const userRef = notAuthedDB.doc("/users/uid");
    await assertFails(userRef.set({ hoge: "hoge" }));
  });

もしrulesを回避して先にテスト用データを用意しておきたいなら、initializeAdminAppを使いましょう*1

firebase.rulesはちゃんと設定している限り自動で読み込まれるので、loadFirestoreRulesbeforeEachで読み込んだりする必要はないはずです。

問題なのが、以下のような関数をテストする場合です。

export const complexTransaction = async () => {
  // 複雑なDB操作...
}

これに関してはスマートな解決策を見つけられていません。一応jestの場合は、

jest.mock("firebase/app", () => {
  // eslint-disable-next-line @typescript-eslint/no-var-requires
  const { initializeTestApp, firestore } = require("@firebase/testing");
  const mockedApp = initializeTestApp({
    projectId: "pid",
    auth: { uid: "uid" },
  });
  return {
    initializeApp: () => mockedApp,
    firestore: firestore,
  };
});

というようにmockすればなんとかテストできますが、あまり綺麗ではないですね。最悪adminでfirebase.firestoreをemulatorにつなげてくれればいいんですが……。

functions

import * as admin from "firebase-admin";
import * as funcTest from "firebase-functions-test";

const db = admin.firestore();
const tester = funcTest({ projectId: process.env.GCLOUD_PROJECT });
const makeDocumentSnapshot = tester.firestore.makeDocumentSnapshot;
const makeChange = tester.makeChange;

const wrapped = tester.wrap(yourAwesomeFunction);
test("assert Firestore", async () => {
  // arrange
  const roomId = "sefasef";
  const roomPath = `rooms/${roomId}`;
  const beforeData = {
    hoge: "hoge",
  };
  const before = makeDocumentSnapshot(beforeData, roomPath);
  const afterData = {
    hoge: "fuga",
  };
  const after = makeDocumentSnapshot(afterData, roomPath);
  const change = makeChange(before, after);
  // act
  await wrapped(change, { params: { roomId } });
  // assert
  const roomRef = db.doc(roomPath);
  expect((await roomRef.get()).data()).toEqual(afterData);
});

こんな感じでいけます。functionsのテストは比較的簡単です。あと、Warning, estimating Firebase Config based on GCLOUD_PROJECT. Initializing firebase-admin may failと警告がでます。これは正直わからん。。。

*1:initializeTestAppのprojectIdと同じもので初期化する必要があります