第19章:テストが本体!EmulatorでRules単体テスト🧪🧯
この章はズバリ、**「Rulesを書いたら、必ず“通る✅/弾く❌”を自動テストで固定する」**回です🙂✨ Security Rules は“門番🚪”なので、ここが揺れると全てが危なくなります…!💥
(公式も Emulator + @firebase/rules-unit-testing での単体テストを強く推しています。認証状態(auth)を疑似的に作れるのが超重要ポイントです🤖🧠)(Firebase)
この章でできるようになること✅
- Emulator 上で Rules を動かして、ローカルで安全に検証できる🧪
- 未ログイン / 一般ユーザー / admin の3パターンをテストで固める👤👮♂️👑
getとlistの違いを、テストで“事故らない形”に固定する📄📚- AI(Gemini CLI / Antigravity)に叩き台を作らせても、最後は人間レビュー&テストで勝つ💪🤖✅ (Firebase)
1) 今日の流れ(最短ルート)🗺️

- Emulator を「テスト実行のたびに起動→テスト→終了」できる形にする🚀
@firebase/rules-unit-testingで auth を偽装してテストを書く🧪- 通るべき✅ / 弾くべき❌ をセットで増やす(TDDっぽく)🧠
- AIに作らせるなら、テストが先に赤→緑になるよう誘導する🤖🟩
Emulator は emulators:exec を使うと、起動→実行→終了が一発でできます(CIにも相性◎)。(Firebase)
2) 手を動かす:Rulesテスト用の“最小セット”を作る🧰✨
2-1. firebase.json に Rules ファイルを必ず紐づける⚠️

ここ、事故ポイントです😇
firebase.json に Rules のファイルパスが無いと、エミュレータが“開放ルール扱い”になりうるので、テストが意味を失います💥(Firebase)
例(雰囲気でOK。既にあるなら確認だけでOK)👇
{
"firestore": {
"rules": "firestore.rules"
},
"emulators": {
"firestore": {
"port": 8080
}
}
}
※ポート 8080 はデフォルト想定です。変えてるならテスト側も合わせます🙂(Firebase)
2-2. Rulesテスト専用の小さなNodeプロジェクトを作る📦
React本体と分けると気持ちがラクです😌(例:tools/rules-tests/)
PowerShell で👇
mkdir tools\rules-tests
cd tools\rules-tests
npm init -y
npm i -D vitest typescript @firebase/rules-unit-testing
npm i firebase
2-3. package.json にテストコマンドを足す▶️
{
"type": "module",
"scripts": {
"test:rules": "vitest run"
}
}
2-4. TS最低設定(1枚だけ)🧩
tools/rules-tests/tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"strict": true,
"types": ["vitest/globals"]
}
}
3) サンプルRules(ロール制御 + owner)🛡️
今回はテストが主役なので、Rules は“題材”としてシンプルにします🙂
(posts コレクション:公開/非公開、owner、admin)
firestore.rules(プロジェクト側にあるものを想定。無ければこれでOK👇)
// firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function signedIn() { return request.auth != null; }
function isAdmin() { return signedIn() && request.auth.token.admin == true; }
function isOwner() { return signedIn() && request.auth.uid == resource.data.ownerUid; }
function isCreatingOwnPost() {
return signedIn()
&& request.resource.data.ownerUid == request.auth.uid;
}
function validTitle() {
return request.resource.data.title is string
&& request.resource.data.title.size() >= 1
&& request.resource.data.title.size() <= 50;
}
match /posts/{postId} {
// 1件取得は「公開 or 本人 or admin」
allow get: if resource.data.isPublic == true || isOwner() || isAdmin();
// 一覧は「adminのみ」(list事故をテストで防ぐ!)
allow list: if isAdmin();
// 作成は「本人のownerUid + title検証」
allow create: if isCreatingOwnPost() && validTitle();
// 更新/削除は「本人 or admin」
allow update, delete: if isOwner() || isAdmin();
}
}
}
4) テストを書く(通る✅/弾く❌ をペアで)🧪✨

tools/rules-tests/tests/posts.rules.test.ts
import { readFileSync } from "node:fs";
import { beforeAll, afterAll, beforeEach, describe, it, expect } from "vitest";
import {
initializeTestEnvironment,
assertFails,
assertSucceeds,
} from "@firebase/rules-unit-testing";
import {
doc,
setDoc,
getDoc,
getDocs,
collection,
query,
} from "firebase/firestore";
const PROJECT_ID = "rules-demo"; // 何でもOK(本番と違う名前がおすすめ)
let testEnv: Awaited<ReturnType<typeof initializeTestEnvironment>>;
beforeAll(async () => {
testEnv = await initializeTestEnvironment({
projectId: PROJECT_ID,
firestore: {
host: "127.0.0.1",
port: 8080,
rules: readFileSync("../../firestore.rules", "utf8"), // ←パスは自分の構成に合わせてね
},
});
});
afterAll(async () => {
await testEnv.cleanup();
});
beforeEach(async () => {
// 毎回まっさらにして、テストが影響し合わないようにする🧼
await testEnv.clearFirestore();
// ルール無視で初期データ投入(Arrange)
await testEnv.withSecurityRulesDisabled(async (context) => {
const db = context.firestore();
await setDoc(doc(db, "posts/public1"), {
ownerUid: "alice",
title: "Hello",
isPublic: true,
});
await setDoc(doc(db, "posts/private1"), {
ownerUid: "alice",
title: "Secret",
isPublic: false,
});
});
});
describe("posts rules", () => {
it("未ログインは公開だけ読める✅", async () => {
const ctx = testEnv.unauthenticatedContext();
const db = ctx.firestore();
await assertSucceeds(getDoc(doc(db, "posts/public1")));
});
it("未ログインは非公開は読めない❌", async () => {
const ctx = testEnv.unauthenticatedContext();
const db = ctx.firestore();
await assertFails(getDoc(doc(db, "posts/private1")));
});
it("本人(alice)は自分の非公開を読める✅", async () => {
const ctx = testEnv.authenticatedContext("alice");
const db = ctx.firestore();
await assertSucceeds(getDoc(doc(db, "posts/private1")));
});
it("他人(bob)はaliceの非公開を読めない❌", async () => {
const ctx = testEnv.authenticatedContext("bob");
const db = ctx.firestore();
await assertFails(getDoc(doc(db, "posts/private1")));
});
it("一覧(list)はadminだけ✅ / 一般は❌", async () => {
const adminCtx = testEnv.authenticatedContext("root", { admin: true });
const userCtx = testEnv.authenticatedContext("alice");
const adminDb = adminCtx.firestore();
const userDb = userCtx.firestore();
await assertSucceeds(getDocs(query(collection(adminDb, "posts"))));
await assertFails(getDocs(query(collection(userDb, "posts"))));
});
it("作成(create):本人ownerUid + title OKなら✅", async () => {
const ctx = testEnv.authenticatedContext("alice");
const db = ctx.firestore();
await assertSucceeds(
setDoc(doc(db, "posts/new1"), {
ownerUid: "alice",
title: "New Post",
isPublic: false,
})
);
});
it("作成(create):ownerUidが本人と違うなら❌", async () => {
const ctx = testEnv.authenticatedContext("alice");
const db = ctx.firestore();
await assertFails(
setDoc(doc(db, "posts/bad1"), {
ownerUid: "bob",
title: "Hacked",
isPublic: false,
})
);
});
it("作成(create):title長すぎは❌", async () => {
const ctx = testEnv.authenticatedContext("alice");
const db = ctx.firestore();
await assertFails(
setDoc(doc(db, "posts/bad2"), {
ownerUid: "alice",
title: "x".repeat(51),
isPublic: false,
})
);
});
});

ポイントまとめ👇😊
assertSucceedsとassertFailsは必ずセットにする(片方だけだと穴が空く🕳️)- データ投入は
withSecurityRulesDisabledでやる(テスト準備のための“神の手”👐) getとlistは別世界なので、必ず両方テストする📄📚
(@firebase/rules-unit-testing が auth モックやデータ消去をサポートするのがキモです。)(Firebase)
5) 実行する🏃♂️💨(毎回安全に:emulators:exec 推奨)

エミュレータを起動しっぱなしにしなくてOK!✨
プロジェクトルート(firebase.json がある場所)で👇
firebase emulators:exec --only firestore "npm --prefix tools/rules-tests run test:rules"
これで👇
- Firestore Emulator 起動🚀
- テスト実行🧪
- 終わったら自動で終了🛑
という流れになります。公式もこの使い方を案内しています。(Firebase)
6) よくある落とし穴(ここだけ見ればだいたい直る😇)🧯
落とし穴A:テストが全部通るのに、なんか怖い…

→ firebase.json の firestore.rules が抜けてる/パス違いの可能性大です⚠️
エミュレータが Rules を読めてないと“開放扱い”になりうるので、まずここ確認!(Firebase)
落とし穴B:Admin SDK で試したら Rules が効かない!
→ それ正常です🙂 サーバー用ライブラリ(Admin/Server)は Rules をバイパスするので、サーバー側は IAM など別の守りが必要です。(Firebase)
7) AIで加速する(ただし“テストが王様”👑)🤖✅
7-1. Gemini CLI:Rulesとテストを“自動で叩き台生成”🧠⚡

Firebase の AI プロンプトは Gemini CLI 拡張から使えて、 Rules とテストの雛形を生成 → テスト実行結果を見て修正 → デプロイまでの導線が用意されています。(Firebase)
公式手順の要点だけ抜くと👇
- 拡張を入れる
- Gemini CLI を起動
/firestore:generate_security_rulesを実行firestore.rulesと、テスト用のsecurity_rules_test_firestoreディレクトリが生成される
…という流れです。(Firebase)
💡注意:コンソール内の “Gemini in Firebase” は Rules 生成に対応してない、という制限も明記されています。(Firebase)
7-2. Antigravity:MCPで「プロジェクトを見ながら」整備🧰✨
Antigravity 側に Firebase MCP server を追加して、エージェントに 「Rulesのテストを増やして」「この失敗の理由を説明して」みたいに頼む使い方が紹介されています。(The Firebase Blog)
おすすめの頼み方(例)🙂
- 「posts の
listは admin のみ。一般ユーザーの list を弾くテストを追加して」 - 「title検証(1〜50文字)の境界値テスト(0,1,50,51)を作って」
- 「この失敗ログから、Rules側の原因候補を3つ出して」
8) ミニ課題🎯(10〜20分)
次の3つを“テスト追加だけで”完成させてください🙂✨
- 境界値テストを追加(title:0/1/50/51)🔤
- adminは非公開を誰のでもgetできる✅ を追加👑
- updateは owner/admin だけ✅ を追加(bob が alice の post を update しようとして ❌)🛡️
9) チェック✅(できたら勝ち!🎉)
-
emulators:execで 毎回まっさらからテストが走る - 未ログイン / user / admin の3者テストがある
-
getとlistが両方テストされてる - “通る✅”と“弾く❌”がペアで増やせる
- AIでRulesを直したら、テストも同時に増やす癖がついた🤖✅
おまけ:サーバー側(Admin SDK)を絡める時のバージョン感🧾
(この章の主役は Rules テストだけど、ロール付与などで Admin SDK を触るなら目安として)
- Functions の Node は 20 / 22 がフルサポート、18は非推奨扱いの流れです。(Firebase)
- Admin SDK(.NET)は .NET 8 以上推奨(6/7 非推奨)。(Firebase)
- Admin SDK(Python)は Python 3.10 以上推奨(3.9 非推奨)。(Firebase)
次の第20章は「AIでRules作成を加速(でも必ず人間レビュー)」なので、今日作ったテスト一式が、そのまま“安全ブレーキ🧯”として超活躍します🙂✨