Firebase Emulatorを使った単体テストについて、
いろいろ調べてみたときの備忘録(*´ω`*)
@firebase/rules-unit-testingを使えばOK(*´ω`*)
環境的には、この記事と一緒でVitestをつかってる
テストしたいルール
firestore.rules
はこんな感じ
// ./firestore.rules rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /users/{uid} { allow read: if request.auth.uid == uid; allow write: if request.auth.uid == uid; } match /@users/{uid} { allow read: if request.auth.uid == uid; allow write: if request.auth.uid == uid; } // Default Rules match /{document=**} { allow read, write: if false; } } }
テストコード
テストコードはこんな感じ
// ./__tests__/security-rule.test.ts import { RulesTestEnvironment, assertFails, assertSucceeds, initializeTestEnvironment, } from "@firebase/rules-unit-testing"; import { Firestore, doc, setDoc } from "firebase/firestore"; import fs from "node:fs"; import { resolve } from "pathe"; import { ulid } from "ulid"; import { afterAll, afterEach, beforeAll, beforeEach, describe, it, } from "vitest"; // utilな関数 async function setDocSuccess(fb: Firestore, path: string, data: object) { await assertSucceeds(setDoc(doc(fb, path), data)); } async function setDocFails(fb: Firestore, path: string, data: object) { await assertFails(setDoc(doc(fb, path), data)); } // テスト本体 describe("firebase.security-rule", async () => { let testEnv: RulesTestEnvironment | null = null; // TestEnvironmentの初期化とfirestore.rulesの読み込み beforeAll(async () => { const filePath = resolve("./firestore.rules"); const rules = fs.readFileSync(filePath, "utf8"); testEnv = await initializeTestEnvironment({ projectId: "my-test-project", firestore: { rules }, }); }); afterAll(async () => { // テスト環境で作成されたすべての RulesTestContexts を破棄 testEnv?.cleanup(); }); beforeEach(async () => {}); afterEach(async () => { // Firestoreのクリア await testEnv?.clearFirestore(); }); // 認証時のテスト it("test-context-auth", async () => { const uid = ulid(); const user = testEnv.authenticatedContext(uid); const firestore = user.firestore() as unknown as Firestore; await setDocSuccess(firestore, `/users/${uid}`, { data: "data" }); await setDocSuccess(firestore, `/@users/${uid}`, { data: "data" }); }); // 未認証時のテスト it("test-context-guest", async () => { const uid = ulid(); const user = testEnv.unauthenticatedContext(); const firestore = user.firestore() as unknown as Firestore; await setDocFails(firestore, `/users/${uid}`, { data: "data" }); await setDocFails(firestore, `/@users/${uid}`, { data: "data" }); }); });
テストを実行
firebase emulators:exec
を使うと、
エミュレータを起動しつつ、テストを実行できる。
長くなりがちなので、package.json
のscripts
を用意。
{ "scripts": { "test": "env-cmd -f .env.test pnpm em:run 'pnpm vitest-es'", "em:run": "firebase emulators:exec --only auth,firestore --project=my-test-project", "vitest-es": "NODE_OPTIONS=\"--enable-source-maps --experimental-vm-modules\" vitest --single-thread --run" }, }
あとは、実行すればOK
$ pnpm test ./__tests__/security-rule.test.ts
ハマったポイント
同じコンテキストからfirebase()を複数回取得はNG
こんな感じで、同じRulesTestContextから
複数回firestore()
を取得しようとすると
it("...", async () => { const user = testEnv.authenticatedContext(uild()); await setDocFails(user.firestore(), `/users/${uid}`, { data: "data" }); await setDocFails(user.firestore(), `/@users/${uid}`, { data: "data" }); });
こんなエラーになる。。
FirebaseError: Firestore has already been started and its settings can no longer be changed. You can only modify settings before calling any other methods on a Firestore object.
なので、こんな感じであればOK。
it("...", async () => { const user = testEnv.authenticatedContext(uild()); const user2 = testEnv.authenticatedContext(uild()); await setDocFails(user.firestore(), `/users/${uid}`, { data: "data" }); await setDocFails(user2.firestore(), `/@users/${uid}`, { data: "data" }); });
もちろん、RulesTestContextをbeforeAll
とかで作成して、使い回す場合も同じく問題になるので注意。
以上!! 便利。。(´ω`)