Catch up on everything we announced at this year's Firebase Summit. Learn more

构建单元测试

Firebase Local Emulator Suite 让您可以更轻松地全面验证应用的功能和行为。此外,它也是验证 Firebase 安全规则配置的绝佳工具。您可以使用 Firebase Emulators 在本地环境中运行单元测试并实现单元测试自动化。在您为自己的应用构建可验证规则的单元测试并实现自动测试的过程中,本文档介绍的方法应该会有所帮助。

如果您尚未设置 Firebase Emulators,请进行设置。

运行模拟器之前的注意事项

开始使用模拟器之前,请注意以下几点:

  • 模拟器最初将加载 firebase.json 文件的 firestore.rules 或“storage.rules”字段中指定的规则。如果该文件不存在,并且您没有按如下所述使用 loadFirestoreRules 或“loadStorageRules”方法,则模拟器会将所有项目视为采用开放规则。
  • 尽管大多数 Firebase SDK 都直接支持模拟器,但只有 @firebase/rules-unit-testing 库支持模拟安全规则中的 auth,这样更便于进行单元测试。此外,该库还支持几项特定于模拟器的功能(例如清除所有数据),具体如下所示。
  • 模拟器还将接受通过客户端 SDK 提供的生产 Firebase Auth 令牌,并据此评估规则,从而可以在集成和手动测试中将您的应用直接连接到模拟器。

数据库模拟器与生产环境之间的差异

  • 您不必明确地创建数据库实例。模拟器会自动创建任何所访问的数据库实例。
  • 每个新数据库在启动时都使用封闭规则,因此非管理员用户将无法执行读取或写入操作。
  • 每个模拟数据库都受 Spark 方案限制和配额约束(最需要注意的是,这会将每个实例的并发连接限制为 100 个)。
  • 任何数据库都将接受字符串 "owner" 作为管理员身份验证令牌。
  • 模拟器目前没有与其他 Firebase 产品进行有效交互。尤其值得注意的是,模拟器不支持标准的 Firebase Authentication 流程。 您可以改为使用 rules-unit-testing 库中的 initializeTestApp() 方法,该方法接受 auth 字段。使用此方法创建的 Firebase 对象的行为类似您提供的任何一个成功地通过了身份验证的实体。如果您传入 null,其行为将与未经身份验证的用户相同(例如,auth != null 规则将失败)。

与 Realtime Database 模拟器进行交互

Firebase Realtime Database 生产实例可通过 firebaseio.com 的子网域访问,您可以按如下所示访问 REST API:

https://<database_name>.firebaseio.com/path/to/my/data.json

模拟器在本地运行,并可通过 localhost:9000 访问。如需与特定数据库实例互动,您必须使用 ns 查询参数指定数据库名称。

http://localhost:9000/path/to/my/data.json?ns=<database_name>

使用 JavaScript SDK 版本 9 运行本地单元测试

Firebase 在其 JavaScript SDK 版本 9 及其 SDK 版本 8 中都分发了安全规则单元测试库。这两种库 API 具有明显的差异。我们建议使用版本 9 测试库,该库更简单些,连接到模拟器时需要的设置更少,从而可以安全地避免意外使用生产资源。为了实现向后兼容性,我们将继续提供版本 8 测试库

使用 @firebase/rules-unit-testing 模块与本地运行的模拟器交互。如果您遇到超时或 ECONNREFUSED 错误,请仔细检查模拟器是否确实正在运行。

我们强烈建议您使用最新版本的 Node.js,以便使用 async/await 表示法。几乎所有您可能希望测试的行为都涉及异步函数,并且测试模块设计为可与基于 Promise 的代码配合使用。

版本 9 规则单元测试库始终知晓模拟器的存在,且绝不会影响生产资源。

您可以使用版本 9 模块化导入语句导入该库。例如:

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.authenticatedContextRulesTestEnvironment.unauthenticatedContext 来模拟身份验证状态的测试用例。

常用的方法和工具函数

另请参阅 SDK 版本 9 中专用于模拟器的测试方法

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,其行为与经过身份验证的 Authentication 用户类似。通过返回的上下文创建的请求将附加模拟的 Authentication 令牌。(可选)为 Authentication 令牌载荷传递一个用于指定自定义声明或替换值的对象。

在测试中使用返回的测试上下文对象来访问配置的任何模拟器实例,包括使用 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,其行为方式与未通过 Authentication 登录的客户端类似。通过返回的上下文创建的请求不会附加 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()

在行为看起来像停用了安全规则的上下文中运行测试设置函数。

此方法采用一个回调函数,该函数会采用绕过安全规则的上下文并返回 promise。在解决/拒绝 Promise 之后,上下文将被销毁。

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'), { ... });

专用于模拟器的方法

另请参阅 SDK 版本 9 中的常用测试方法和工具函数

Cloud Firestore

Cloud Firestore

RulesTestEnvironment.clearFirestore() => Promise<void>

此方法会清除 Firestore 数据库中属于为 Firestore 模拟器配置的 projectId 的数据。

RulesTestContext.firestore(settings?: Firestore.FirestoreSettings) => Firestore;

此方法会获取用于此测试上下文的 Firestore 实例。返回的 Firebase JS 客户端 SDK 实例可以与客户端 SDK API(版本 9 模块化 API 或版本 9 兼容性 API)搭配使用。

Realtime Database

Realtime Database

RulesTestEnvironment.clearDatabase() => Promise<void>

此方法会清除 Realtime Database 中属于为 Realtime Database 模拟器配置的 projectId 的数据。

RulesTestContext.database(databaseURL?: Firestore.FirestoreSettings) => Firestore;

获取用于此测试上下文的 Realtime Database 实例。返回的 Firebase JS 客户端 SDK 实例可以与客户端 SDK API(版本 9 模块化 API 或版本 9 兼容性 API)搭配使用。该方法接受 Realtime Database 实例的网址。如果指定,则会返回模拟版本的命名空间的实例,以及从网址中提取的参数。

Cloud Storage

Cloud Storage

RulesTestEnvironment.clearStorage() => Promise<void>

此方法会清除存储桶中属于为 Cloud Storage 模拟器配置的 projectId 的对象和元数据。

RulesTestContext.storage(bucketUrl?: string) => Firebase Storage;

此方法会返回配置为连接到模拟器的 Storage 实例。 另外,此方法会接受 Firebase Storage 存储桶的 gs:// 网址以进行测试。如果指定,则会返回模拟版本的存储桶名称的 Storage 实例。

使用 JavaScript SDK 版本 8 运行本地单元测试

选择一种产品,查看 Firebase Test SDK 与模拟器交互所使用的方法。

Cloud Firestore

initializeTestApp({ projectId: string, auth: Object }) => FirebaseApp

此方法会返回一个初始化的 Firebase 应用,该应用与选项中指定的项目 ID 和 auth 变量相对应。使用此方法可创建以特定用户身份通过身份验证的应用,以用于测试。

firebase.initializeTestApp({
  projectId: "my-test-project",
  auth: { uid: "alice", email: "alice@example.com" }
});

initializeAdminApp({ projectId: string }) => FirebaseApp

此方法会返回一个初始化的管理员 Firebase 应用。此应用执行读取和写入操作时会绕过安全规则。使用此方法可创建以管理员身份通过了身份验证的应用以设置测试状态。

firebase.initializeAdminApp({ projectId: "my-test-project" });
    

apps() => [FirebaseApp] 此方法会返回所有当前已经初始化的测试应用和管理应用。使用此方法可在各次测试之间或测试之后清理应用。

Promise.all(firebase.apps().map(app => app.delete()))

loadFirestoreRules({ projectId: string, rules: Object }) => Promise

此方法可以将规则发送到本地运行的数据库。它接受一个以字符串描述这些规则的对象。 使用此方法可设置数据库的规则。

firebase.loadFirestoreRules({
  projectId: "my-test-project",
  rules: fs.readFileSync("/path/to/firestore.rules", "utf8")
});
    

assertFails(pr: Promise) => Promise

此方法会返回一个在输入成功时遭拒或在输入遭拒时成功的 promise。使用此方法判断数据库读取或写入是否失败。

firebase.assertFails(app.firestore().collection("private").doc("super-secret-document").get());
    

assertSucceeds(pr: Promise) => Promise

此方法会返回一个在输入成功时成功并在输入遭拒时遭拒的 promise。使用此方法判断数据库读取或写入是否成功。

firebase.assertSucceeds(app.firestore().collection("public").doc("test-document").get());
    

clearFirestoreData({ projectId: string }) => Promise

此方法会清除与本地运行的 Firestore 实例中的特定项目相关联的所有数据。使用此方法可在测试之后进行清理。

firebase.clearFirestoreData({
  projectId: "my-test-project"
});
   

Realtime Database

Realtime Database

initializeTestApp({ databaseName: string, auth: Object }) => FirebaseApp

使用此方法可创建以特定用户身份通过身份验证的应用,以用于测试。

此方法会返回一个初始化的 Firebase 应用,该应用与选项中指定的数据库名称和 auth 变量替换值相对应。

firebase.initializeTestApp({
  databaseName: "my-database",
  auth: { uid: "alice" }
});

initializeAdminApp({ databaseName: string }) => FirebaseApp

使用此方法可创建以管理员身份通过身份验证的应用,以设置测试状态。

此方法会返回一个初始化的 Firebase 管理应用,该应用与选项中指定的数据库名称相对应。在从数据库读取数据和向数据库写入数据时,该应用会绕过安全规则。

firebase.initializeAdminApp({ databaseName: "my-database" });

loadDatabaseRules({ databaseName: string, rules: Object }) => Promise

使用此方法可设置数据库的规则。

此方法可以将规则发送到本地运行的数据库,它接受一个以字符串表示“databaseName”和“rules”的选项对象。

firebase
      .loadDatabaseRules({
        databaseName: "my-database",
        rules: "{'rules': {'.read': false, '.write': false}}"
      });

apps() => [FirebaseApp]

返回目前已初始化的所有测试应用和管理应用。

使用此方法可在测试之间或测试之后清理应用(请注意,带有处于活跃状态的监听器的已初始化应用会阻止 JavaScript 退出):

 Promise.all(firebase.apps().map(app => app.delete()))

assertFails(pr: Promise) => Promise

返回一个在输入成功时遭拒并在输入遭拒时成功的 promise。

使用此方法判断数据库读取或写入是否失败。

firebase.assertFails(app.database().ref("secret").once("value"));

assertSucceeds(pr: Promise) => Promise

返回一个在输入成功时成功并在输入遭拒时遭拒的 promise。

使用此方法判断数据库读取或写入是否成功:

firebase.assertSucceeds(app.database().ref("public").once("value"));

Cloud Storage

Cloud Storage

initializeTestApp({ storageBucket: string, auth: Object }) => FirebaseApp

使用此方法可创建以特定用户身份通过身份验证的应用,以用于测试。

此方法会返回一个初始化的 Firebase 应用,该应用与选项中指定的存储桶名称和 auth 变量替换值相对应。

firebase.initializeTestApp({
  storageBucket: "my-bucket",
  auth: { uid: "alice" }
});

initializeAdminApp({ storageBucket: string }) => FirebaseApp

使用此方法可创建以管理员身份通过身份验证的应用,以设置测试状态。

此方法会返回一个初始化的 Firebase 管理应用,该应用与选项中指定的存储桶名称相对应。在从存储桶读取数据和向存储桶写入数据时,该应用会绕过安全规则。

firebase.initializeAdminApp({ storageBucket: "my-bucket" });

loadStorageRules({ storageBucket: string, rules: Object }) => Promise

使用此方法可设置存储桶的规则。

将规则发送到本地代管的存储桶。它接受一个以字符串表示“storageBucket”和“rules”的选项对象。

firebase
      .loadStorageRules({
        storageBucket: "my-bucket",
        rules: fs.readFileSync("/path/to/storage.rules", "utf8")
      });

apps() => [FirebaseApp]

返回目前已初始化的所有测试应用和管理应用。

使用此方法可在测试之间或测试之后清理应用(请注意,带有处于活跃状态的监听器的已初始化应用会阻止 JavaScript 退出):

 Promise.all(firebase.apps().map(app => app.delete()))

assertFails(pr: Promise) => Promise

返回一个在输入成功时遭拒并在输入遭拒时成功的 promise。

使用此方法判断存储桶读取或写入是否失败:

firebase.assertFails(app.storage().ref("letters/private.doc").getMetadata());

assertSucceeds(pr: Promise) => Promise

返回一个在输入成功时成功并在输入遭拒时遭拒的 promise。

使用此方法判断存储桶读取或写入是否成功:

firebase.assertFails(app.storage().ref("images/cat.png").getMetadata());

适用于 JS SDK 版本 8 的 RUT 库 API

选择一种产品,查看 Firebase Test SDK 与模拟器交互所使用的方法。

Cloud Firestore

Cloud Firestore

initializeTestApp({ projectId: string, auth: Object }) => FirebaseApp

此方法会返回一个初始化的 Firebase 应用,该应用与选项中指定的项目 ID 和 auth 变量相对应。使用此方法可创建以特定用户身份通过身份验证的应用,以用于测试。

firebase.initializeTestApp({
  projectId: "my-test-project",
  auth: { uid: "alice", email: "alice@example.com" }
});

initializeAdminApp({ projectId: string }) => FirebaseApp

此方法会返回一个初始化的管理员 Firebase 应用。此应用执行读取和写入操作时会绕过安全规则。使用此方法可创建以管理员身份通过了身份验证的应用以设置测试状态。

firebase.initializeAdminApp({ projectId: "my-test-project" });
    

apps() => [FirebaseApp] 此方法会返回所有当前已经初始化的测试应用和管理应用。使用此方法可在各次测试之间或测试之后清理应用。

Promise.all(firebase.apps().map(app => app.delete()))

loadFirestoreRules({ projectId: string, rules: Object }) => Promise

此方法可以将规则发送到本地运行的数据库。它接受一个以字符串描述这些规则的对象。 使用此方法可设置数据库的规则。

firebase.loadFirestoreRules({
  projectId: "my-test-project",
  rules: fs.readFileSync("/path/to/firestore.rules", "utf8")
});
    

assertFails(pr: Promise) => Promise

此方法会返回一个在输入成功时遭拒或在输入遭拒时成功的 promise。使用此方法判断数据库读取或写入是否失败。

firebase.assertFails(app.firestore().collection("private").doc("super-secret-document").get());
    

assertSucceeds(pr: Promise) => Promise

此方法会返回一个在输入成功时成功并在输入遭拒时遭拒的 promise。使用此方法判断数据库读取或写入是否成功。

firebase.assertSucceeds(app.firestore().collection("public").doc("test-document").get());
    

clearFirestoreData({ projectId: string }) => Promise

此方法会清除与本地运行的 Firestore 实例中的特定项目相关联的所有数据。使用此方法可在测试之后进行清理。

firebase.clearFirestoreData({
  projectId: "my-test-project"
});
   

Realtime Database

Realtime Database

initializeTestApp({ databaseName: string, auth: Object }) => FirebaseApp

使用此方法可创建以特定用户身份通过身份验证的应用,以用于测试。

此方法会返回一个初始化的 Firebase 应用,该应用与选项中指定的数据库名称和 auth 变量替换值相对应。

firebase.initializeTestApp({
  databaseName: "my-database",
  auth: { uid: "alice" }
});

initializeAdminApp({ databaseName: string }) => FirebaseApp

使用此方法可创建以管理员身份通过身份验证的应用,以设置测试状态。

此方法会返回一个初始化的 Firebase 管理应用,该应用与选项中指定的数据库名称相对应。在从数据库读取数据和向数据库写入数据时,该应用会绕过安全规则。

firebase.initializeAdminApp({ databaseName: "my-database" });

loadDatabaseRules({ databaseName: string, rules: Object }) => Promise

使用此方法可设置数据库的规则。

此方法可以将规则发送到本地运行的数据库,它接受一个以字符串表示“databaseName”和“rules”的选项对象。

firebase
      .loadDatabaseRules({
        databaseName: "my-database",
        rules: "{'rules': {'.read': false, '.write': false}}"
      });

apps() => [FirebaseApp]

返回目前已初始化的所有测试应用和管理应用。

使用此方法可在测试之间或测试之后清理应用(请注意,带有处于活跃状态的监听器的已初始化应用会阻止 JavaScript 退出):

 Promise.all(firebase.apps().map(app => app.delete()))

assertFails(pr: Promise) => Promise

返回一个在输入成功时遭拒并在输入遭拒时成功的 promise。

使用此方法判断数据库读取或写入是否失败。

firebase.assertFails(app.database().ref("secret").once("value"));

assertSucceeds(pr: Promise) => Promise

返回一个在输入成功时成功并在输入遭拒时遭拒的 promise。

使用此方法判断数据库读取或写入是否成功:

firebase.assertSucceeds(app.database().ref("public").once("value"));

Cloud Storage

Cloud Storage

initializeTestApp({ storageBucket: string, auth: Object }) => FirebaseApp

使用此方法可创建以特定用户身份通过身份验证的应用,以用于测试。

此方法会返回一个初始化的 Firebase 应用,该应用与选项中指定的存储桶名称和 auth 变量替换值相对应。

firebase.initializeTestApp({
  storageBucket: "my-bucket",
  auth: { uid: "alice" }
});

initializeAdminApp({ storageBucket: string }) => FirebaseApp

使用此方法可创建以管理员身份通过身份验证的应用,以设置测试状态。

此方法会返回一个初始化的 Firebase 管理应用,该应用与选项中指定的存储桶名称相对应。在从存储桶读取数据和向存储桶写入数据时,该应用会绕过安全规则。

firebase.initializeAdminApp({ storageBucket: "my-bucket" });

loadStorageRules({ storageBucket: string, rules: Object }) => Promise

使用此方法可设置存储桶的规则。

将规则发送到本地代管的存储桶。它接受一个以字符串表示“storageBucket”和“rules”的选项对象。

firebase
      .loadStorageRules({
        storageBucket: "my-bucket",
        rules: fs.readFileSync("/path/to/storage.rules", "utf8")
      });

apps() => [FirebaseApp]

返回目前已初始化的所有测试应用和管理应用。

使用此方法可在测试之间或测试之后清理应用(请注意,带有处于活跃状态的监听器的已初始化应用会阻止 JavaScript 退出):

 Promise.all(firebase.apps().map(app => app.delete()))

assertFails(pr: Promise) => Promise

返回一个在输入成功时遭拒并在输入遭拒时成功的 promise。

使用此方法判断存储桶读取或写入是否失败:

firebase.assertFails(app.storage().ref("letters/private.doc").getMetadata());

assertSucceeds(pr: Promise) => Promise

返回一个在输入成功时成功并在输入遭拒时遭拒的 promise。

使用此方法判断存储桶读取或写入是否成功:

firebase.assertFails(app.storage().ref("images/cat.png").getMetadata());