在构建应用时,您可能希望锁定对 Cloud Firestore 数据库的访问。但是,在启动之前,您需要更细致的 Cloud Firestore 安全规则。使用 Cloud Firestore 模拟器,除了原型设计和测试应用程序的一般功能和行为之外,您还可以编写单元测试来检查 Cloud Firestore 安全规则的行为。
快速开始
对于一些具有简单规则的基本测试用例,请试用快速入门示例。
了解 Cloud Firestore 安全规则
在使用移动和 Web 客户端库时,实施Firebase 身份验证和Cloud Firestore 安全规则以进行无服务器身份验证、授权和数据验证。
Cloud Firestore 安全规则包括两部分:
- 标识数据库中文档的
match
语句。 - 控制对这些文档的访问的
allow
表达式。
Firebase 身份验证验证用户的凭据并为基于用户和基于角色的访问系统提供基础。
在读取或写入任何数据之前,来自 Cloud Firestore 移动/Web 客户端库的每个数据库请求都会根据您的安全规则进行评估。如果规则拒绝访问任何指定的文档路径,则整个请求将失败。
在开始使用 Cloud Firestore 安全规则 中了解有关 Cloud Firestore 安全规则的更多信息。
安装模拟器
要安装 Cloud Firestore 模拟器,请使用Firebase CLI并运行以下命令:
firebase setup:emulators:firestore
运行模拟器
首先在您的工作目录中初始化一个 Firebase 项目。这是使用 Firebase CLI时常见的第一步。
firebase init
使用以下命令启动模拟器。模拟器将一直运行,直到您终止该进程:
firebase emulators:start --only firestore
在许多情况下,您希望启动模拟器,运行测试套件,然后在测试运行后关闭模拟器。您可以使用emulators:exec
命令轻松完成此操作:
firebase emulators:exec --only firestore "./my-test-script.sh"
启动时,模拟器将尝试在默认端口 (8080) 上运行。您可以通过修改firebase.json
文件的"emulators"
部分来更改模拟器端口:
{ // ... "emulators": { "firestore": { "port": "YOUR_PORT" } } }
运行模拟器之前
在开始使用模拟器之前,请记住以下几点:
- 模拟器最初会加载在
firebase.json
文件的firestore.rules
字段中指定的规则。它需要包含您的 Cloud Firestore 安全规则的本地文件的名称,并将这些规则应用于所有项目。如果您不提供本地文件路径或使用如下所述的loadFirestoreRules
方法,模拟器会将所有项目视为具有开放规则。 - 虽然大多数 Firebase SDK直接与模拟器一起使用,但只有
@firebase/rules-unit-testing
库支持安全规则中的模拟auth
,从而使单元测试更加容易。此外,该库还支持一些特定于模拟器的功能,例如清除所有数据,如下所列。 - 模拟器还将接受通过客户端 SDK 提供的生产 Firebase Auth 令牌并相应地评估规则,这允许您的应用程序在集成和手动测试中直接连接到模拟器。
运行本地单元测试
使用 v9 JavaScript SDK 运行本地单元测试
Firebase 分发了一个安全规则单元测试库,其中包含第 9 版 JavaScript SDK 和第 8 版 SDK。库 API 明显不同。我们推荐 v9 测试库,它更加精简并且需要更少的设置来连接到模拟器,从而安全地避免意外使用生产资源。为了向后兼容,我们继续提供v8 测试库。
使用@firebase/rules-unit-testing
模块与本地运行的模拟器交互。如果出现超时或ECONNREFUSED
错误,请仔细检查模拟器是否确实在运行。
我们强烈建议使用最新版本的 Node.js,以便您可以使用async/await
表示法。您可能想要测试的几乎所有行为都涉及异步函数,并且测试模块旨在与基于 Promise 的代码一起使用。
v9 规则单元测试库始终了解模拟器并且从不接触您的生产资源。
您使用 v9 模块化导入语句导入库。例如:
import {
assertFails,
assertSucceeds,
initializeTestEnvironment,
RulesTestEnvironment,
} from "@firebase/rules-unit-testing"
// Use `const { … } = require("@firebase/rules-unit-testing")` if imports are not supported
// Or we suggest `const testing = require("@firebase/rules-unit-testing")` if necessary.
导入后,实施单元测试涉及:
- 通过调用
initializeTestEnvironment
创建和配置RulesTestEnvironment
。 - 在不触发规则的情况下设置测试数据,使用允许您暂时绕过它们的便捷方法
RulesTestEnvironment.withSecurityRulesDisabled
。 - 设置测试套件和每个测试之前/之后挂钩,调用清理测试数据和环境,如
RulesTestEnvironment.cleanup()
或RulesTestEnvironment.clearFirestore()
。 - 使用
RulesTestEnvironment.authenticatedContext
和RulesTestEnvironment.unauthenticatedContext
实现模拟身份验证状态的测试用例。
常用方法和实用函数
另请参阅v9 SDK 中特定于模拟器的测试方法。
initializeTestEnvironment() => RulesTestEnvironment
此函数为规则单元测试初始化测试环境。首先调用此函数进行测试设置。成功执行需要运行模拟器。
该函数接受定义TestEnvironmentConfig
的可选对象,该对象可以包含项目 ID 和模拟器配置设置。
let testEnv = await initializeTestEnvironment({ projectId: "demo-project-1234", firestore: { rules: fs.readFileSync("firestore.rules", "utf8"), }, });
RulesTestEnvironment.authenticatedContext({ user_id: string, tokenOptions?: TokenOptions }) => RulesTestContext
此方法创建一个RulesTestContext
,其行为类似于经过身份验证的身份验证用户。通过返回的上下文创建的请求将附加一个模拟身份验证令牌。或者,传递一个定义自定义声明或覆盖身份验证令牌有效负载的对象。
在测试中使用返回的测试上下文对象来访问配置的任何模拟器实例,包括使用initializeTestEnvironment
配置的实例。
// Assuming a Firestore app and the Firestore emulator for this example import { setDoc } from "firebase/firestore"; const alice = testEnv.authenticatedContext("alice", { … }); // Use the Firestore instance associated with this context await assertSucceeds(setDoc(alice.firestore(), '/users/alice'), { ... });
RulesTestEnvironment.unauthenticatedContext() => RulesTestContext
此方法创建一个RulesTestContext
,其行为类似于未通过身份验证登录的客户端。通过返回的上下文创建的请求不会附加 Firebase Auth 令牌。
在测试中使用返回的测试上下文对象来访问配置的任何模拟器实例,包括使用initializeTestEnvironment
配置的实例。
// 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));
RulesTestEnvironment.withSecurityRulesDisabled()
使用上下文运行测试设置函数,其行为就好像安全规则被禁用一样。
此方法采用回调函数,该函数采用绕过安全规则的上下文并返回承诺。一旦承诺解决/拒绝,上下文将被销毁。
RulesTestEnvironment.cleanup()
此方法会销毁在测试环境中创建的所有RulesTestContexts
并清理底层资源,从而允许干净退出。
此方法不会以任何方式更改模拟器的状态。要在测试之间重置数据,请使用特定于应用程序模拟器的清除数据方法。
assertSucceeds(pr: Promise<any>)) => Promise<any>
这是一个测试用例效用函数。
该函数断言所提供的包装模拟器操作的 Promise 将在不违反安全规则的情况下得到解决。
await assertSucceeds(setDoc(alice.firestore(), '/users/alice'), { ... });
assertFails(pr: Promise<any>)) => Promise<any>
这是一个测试用例效用函数。
该函数断言所提供的包装模拟器操作的 Promise 将因违反安全规则而被拒绝。
await assertFails(setDoc(alice.firestore(), '/users/bob'), { ... });
特定于模拟器的方法
另请参阅v9 SDK 中的常见测试方法和实用函数。
RulesTestEnvironment.clearFirestore() => Promise<void>
此方法清除 Firestore 数据库中属于为 Firestore 模拟器配置的projectId
的数据。
RulesTestContext.firestore(settings?: Firestore.FirestoreSettings) => Firestore;
此方法为此测试上下文获取一个 Firestore 实例。返回的 Firebase JS 客户端 SDK 实例可以与客户端 SDK API(v9 模块化或 v9 兼容)一起使用。
可视化规则评估
Cloud Firestore 模拟器可让您在 Emulator Suite UI 中可视化客户端请求,包括 Firebase 安全规则的评估跟踪。
打开Firestore > Requests选项卡以查看每个请求的详细评估序列。
生成测试报告
运行一套测试后,您可以访问测试覆盖率报告,其中显示了如何评估每个安全规则。
要获取报告,请在模拟器运行时查询公开的端点。对于浏览器友好的版本,请使用以下 URL:
http://localhost:8080/emulator/v1/projects/<project_id>:ruleCoverage.html
这会将您的规则分解为表达式和子表达式,您可以将鼠标悬停在它们上以获取更多信息,包括计算次数和返回值。对于此数据的原始 JSON 版本,请在查询中包含以下 URL:
http://localhost:8080/emulator/v1/projects/<project_id>:ruleCoverage
模拟器和生产之间的差异
- 您不必显式创建 Cloud Firestore 项目。模拟器会自动创建任何被访问的实例。
- Cloud Firestore 模拟器不适用于正常的 Firebase 身份验证流程。相反,在 Firebase 测试 SDK 中,我们在
rules-unit-testing
库中提供了initializeTestApp()
方法,该方法采用auth
字段。使用此方法创建的 Firebase 句柄将表现得好像它已成功验证为您提供的任何实体一样。如果您传入null
,它将表现为未经身份验证的用户(例如,auth != null
规则将失败)。
解决已知问题
在使用 Cloud Firestore 模拟器时,您可能会遇到以下已知问题。请按照以下指南对您遇到的任何异常行为进行故障排除。这些说明在编写时考虑了安全规则单元测试库,但一般方法适用于任何 Firebase SDK。
测试行为不一致
如果您的测试偶尔会通过和失败,即使没有对测试本身进行任何更改,您可能也需要验证它们的顺序是否正确。大多数与模拟器的交互都是异步的,因此请仔细检查所有异步代码的顺序是否正确。您可以通过链接承诺或自由使用await
符号来修复顺序。
特别是,查看以下异步操作:
- 设置安全规则,例如,
initializeTestEnvironment
。 - 读取和写入数据,例如,
db.collection("users").doc("alice").get()
。 - 操作断言,包括
assertSucceeds
和assertFails
。
测试仅在您第一次加载模拟器时通过
模拟器是有状态的。它将写入的所有数据存储在内存中,因此只要模拟器关闭,任何数据都会丢失。如果您针对同一个项目 ID 运行多个测试,则每个测试都会产生可能影响后续测试的数据。您可以使用以下任何一种方法来绕过此行为:
- 为每个测试使用唯一的项目 ID。请注意,如果您选择这样做,您将需要调用
initializeTestEnvironment
作为每个测试的一部分;仅为默认项目 ID 自动加载规则。 - 重组您的测试,使它们不与以前写入的数据交互(例如,为每个测试使用不同的集合)。
- 删除测试期间写入的所有数据。
测试设置非常复杂
设置测试时,您可能希望以 Cloud Firestore 安全规则实际上不允许的方式修改数据。如果您的规则使测试设置变得复杂,请尝试在设置步骤中使用RulesTestEnvironment.withSecurityRulesDisabled
,这样读取和写入就不会触发PERMISSION_DENIED
错误。
之后,您的测试可以分别使用RulesTestEnvironment.authenticatedContext
和unauthenticatedContext
作为经过身份验证或未经身份验证的用户执行操作。这使您可以验证您的 Cloud Firestore 安全规则是否正确允许/拒绝不同的情况。