本文概述如何用第九版 JavaScript SDK 和模擬器為應用程式建立和自動化單元測試來驗證 Security Rules。如果還沒有設定 Firebase 模擬器,請設定它

第一步: 使用 Firebase v9

import 	assertFails, 
	assertSucceeds, 
	initializeTestEnvironment
} from "@firebase/rules-unit-testing"

第二步: 建立測試環境

透過呼叫initializeTestEnvironment建立和配置 RulesTestEnvironment 。

let testEnv = await initializeTestEnvironment({ 
	projectId: "demo-project-1234"	firestore: {   
		rules: fs.readFileSync("firestore.rules", "utf8"), 
	}
});

第三部:設定測試資料

這一步使用 RulesTestEnvironment.withSecurityRulesDisabled() ,它用來在測試期間臨時禁用安全規則。這樣的設置允許開發者在進行單元測試或集成測試時,不受安全規則的限制,能夠更自由地測試應用的各個部分。

第四步: Unit Test Setup and TearDown

在設定測試套件時,通常會包括在每個測試執行前後進行特定的動作,這稱為測試的前置和後置掛鉤(hooks)。這些掛鉤的功能是為了確保測試的環境在開始前是乾淨的,以及測試完成後將環境恢復到一個基線狀態。這樣做可以避免測試間的數據污染問題,並保證每個測試的獨立性和可重複性。

  • RulesTestEnvironment.cleanup():這個方法用於測試結束後清理測試環境,移除所有臨時設置和數據,確保不會對後續的測試造成干擾。
  • RulesTestEnvironment.clearFirestore():這個方法專門用於清空 Firestore 的所有文檔和數據集合,是在需要完全重置數據庫狀態時使用。

第五部:實作模擬驗證狀態的 unit test

這一步使用RulesTestEnvironment.authenticatedContext RulesTestEnvironment.unauthenticatedContext實作模擬驗證狀態的測試案例。

使用者驗證後的測試環境

產生驗證某個使用者的 Rule 測試環境

iimport { setDoc } from "firebase/firestore";

const alice = testEnv.authenticatedContext("alice", { … });
// Use the Firestore instance associated with this context
await assertSucceeds(setDoc(alice.firestore(), '/users/alice'), { ... });

上面這個範例,會模擬 alice 登入後,並且對 /users/alice 寫入一個 document。

使用者未登入的測試環境

// Assuming a Cloud Storage app and the Storage emulator for this example
import { getStorage, ref, deleteObject } from "firebase/storage";

const alice = testEnv.unauthenticatedContext();

// Use the Cloud Storage instance associated with this context
const desertRef = ref(alice.storage(), 'images/desert.jpg');
await assertSucceeds(deleteObject(desertRef));

斷言

如同其他 unit test 框架, firebase rule testing 也提供兩個斷言供開發者使用

  • assertSucceeds(pr: Promise<any>)) => Promise<any> :: 確認成功
  • assertFails(pr: Promise<any>)) => Promise<any> :: 確認失敗

考慮一個使用 Cloud Firestore 的範例應用,該應用程式會計算使用者點擊按鈕的次數。該應用程式採用以下規則:

 service cloud.firestore {   
	 match /databases/{database}/documents {     
		 match /counters/{counter} {       
			 allow read;       
			 allow write: if request.resource.data.value == resource.data.value +1;     
		}   
}

若要偵錯上面顯示的規則中的錯誤,請使用下列範例 JavaScript 測試:

const counter0 = db.collection("counters").doc("0");await firebase.assertSucceeds(counter0.set({value: 0}));

接下來來看一個完整的範例,了解怎麼在 vitest 進行 security rules 的測試。Vitest 是一個現代的 JavaScript 測試框架。Vitest 能夠直接運行 ES 模組源碼而無需轉換。此外,Vitest 還支持模擬、快照測試等高級功能,並且可以透過插件輕鬆整合其他工具,如 React Testing Library。

import { afterAll, beforeAll, describe, it } from "vitest";
import fs from "fs";
import {
    RulesTestEnvironment,
    assertFails,
    assertSucceeds,
    initializeTestEnvironment
} from "@firebase/rules-unit-testing"

import { doc, getDoc } from "firebase/firestore";

describe("Firestore rules", () => {

    let testEnv: RulesTestEnvironment;

	// 在所有測試開始之前,設定測試環境
    beforeAll(async () => {

        testEnv = await initializeTestEnvironment({
            projectId: "demo-makerkit",
            firestore: {
                rules: fs.readFileSync(__dirname + "/firestore.rules", "utf8"),
                host: "localhost",
                port: 8080,
            },
        });
    });

	// 在所有測試結束後,清除 Firestore 數據
    afterAll(async () => {
        testEnv.clearFirestore();
    });

	// 測試案例:確認未認證的使用者不可讀取數據
    it("should allow read if authenticated", async () => {
        const alice = testEnv.authenticatedContext("VdoROdrhNMFSGktYdEOzbQOKndnd", {});

        const docRef = doc(alice.firestore(), '/organizations/vrpMguv6cwf7L9KLEknR');

        return assertSucceeds(getDoc(docRef));
    });

	// 測試案例:確認已認證的使用者可以讀取數據
    it("should not allow read if not authenticated", async () => {
        const alice = testEnv.unauthenticatedContext();

        const docRef = doc(alice.firestore(), '/organizations/vrpMguv6cwf7L9KLEknR');

        return assertFails(getDoc(docRef));
    });
});

其他的 unit test framework 也是差不多的概念。

第六步:生成測試報告

Firebase 模擬器能夠產生規則覆蓋率報告,使用以下 URL 閱讀:

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

See Also