1. 开始之前
Cloud Firestore 和 Cloud Functions 等无服务器后端工具非常易于使用,但可能难以测试。 Firebase Local Emulator Suite 允许您在开发机器上运行这些服务的本地版本,以便您可以快速安全地开发您的应用程序。
先决条件
- 一个简单的编辑器,例如 Visual Studio Code、Atom 或 Sublime Text
- Node.js 10.0.0 或更高版本(要安装 Node.js,请使用 nvm ,要检查您的版本,请运行
node --version
) - Java 7 或更高版本(要安装 Java,请使用这些说明,要检查您的版本,请运行
java -version
)
你会做什么
在此 Codelab 中,您将运行和调试一个由多个 Firebase 服务提供支持的简单在线购物应用程序:
- Cloud Firestore:具有实时功能的全球可扩展、无服务器、NoSQL 数据库。
- Cloud Functions :为响应事件或 HTTP 请求而运行的无服务器后端代码。
- Firebase 身份验证:一种与其他 Firebase 产品集成的托管身份验证服务。
- Firebase 托管:快速安全地托管网络应用程序。
您会将应用程序连接到 Emulator Suite 以启用本地开发。
您还将学习如何:
- 如何将您的应用程序连接到模拟器套件以及各种模拟器的连接方式。
- Firebase 安全规则的工作原理以及如何针对本地模拟器测试 Firestore 安全规则。
- 如何编写由 Firestore 事件触发的 Firebase 函数,以及如何编写针对 Emulator Suite 运行的集成测试。
2.设置
获取源代码
在此 Codelab 中,您将从接近完成的 The Fire Store 示例版本开始,因此您需要做的第一件事是克隆源代码:
$ git clone https://github.com/firebase/emulators-codelab.git
然后进入 Codelab 目录,您将在该目录中完成此 Codelab 的剩余部分:
$ cd emulators-codelab/codelab-initial-state
现在,安装依赖项以便您可以运行代码。如果您的互联网连接速度较慢,这可能需要一两分钟:
# Move into the functions directory
$ cd functions
# Install dependencies
$ npm install
# Move back into the previous directory
$ cd ../
获取 Firebase CLI
Emulator Suite 是 Firebase CLI(命令行界面)的一部分,可以使用以下命令将其安装在您的计算机上:
$ npm install -g firebase-tools
接下来,确认您拥有最新版本的 CLI。此 Codelab 应适用于 9.0.0 或更高版本,但更高版本包含更多错误修复。
$ firebase --version 9.6.0
连接到您的 Firebase 项目
如果您没有 Firebase 项目,请在Firebase 控制台中创建一个新的 Firebase 项目。记下您选择的项目 ID,稍后您将需要它。
现在我们需要将此代码连接到您的 Firebase 项目。首先运行以下命令登录 Firebase CLI:
$ firebase login
接下来运行以下命令来创建项目别名。将$YOUR_PROJECT_ID
替换为您的 Firebase 项目的 ID。
$ firebase use $YOUR_PROJECT_ID
现在您已准备好运行该应用程序!
3.运行模拟器
在本节中,您将在本地运行该应用程序。这意味着是时候启动模拟器套件了。
启动模拟器
在 Codelab 源目录中,运行以下命令以启动模拟器:
$ firebase emulators:start --import=./seed
你应该看到这样的输出:
$ firebase emulators:start --import=./seed i emulators: Starting emulators: auth, functions, firestore, hosting ⚠ functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: database, pubsub i firestore: Importing data from /Users/samstern/Projects/emulators-codelab/codelab-initial-state/seed/firestore_export/firestore_export.overall_export_metadata i firestore: Firestore Emulator logging to firestore-debug.log i hosting: Serving hosting files from: public ✔ hosting: Local server: http://127.0.0.1:5000 i ui: Emulator UI logging to ui-debug.log i functions: Watching "/Users/samstern/Projects/emulators-codelab/codelab-initial-state/functions" for Cloud Functions... ✔ functions[calculateCart]: firestore function initialized. ┌─────────────────────────────────────────────────────────────┐ │ ✔ All emulators ready! It is now safe to connect your app. │ │ i View Emulator UI at http://127.0.0.1:4000 │ └─────────────────────────────────────────────────────────────┘ ┌────────────────┬────────────────┬─────────────────────────────────┐ │ Emulator │ Host:Port │ View in Emulator UI │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Functions │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Firestore │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Hosting │ 127.0.0.1:5000 │ n/a │ └────────────────┴────────────────┴─────────────────────────────────┘ Emulator Hub running at 127.0.0.1:4400 Other reserved ports: 4500 Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.
一旦您看到所有模拟器已启动消息,该应用程序就可以使用了。
将网络应用程序连接到模拟器
根据日志中的表格,我们可以看到 Cloud Firestore 模拟器正在侦听端口8080
,而身份验证模拟器正在侦听端口9099
。
┌────────────────┬────────────────┬─────────────────────────────────┐ │ Emulator │ Host:Port │ View in Emulator UI │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Functions │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Firestore │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Hosting │ 127.0.0.1:5000 │ n/a │ └────────────────┴────────────────┴─────────────────────────────────┘
让我们将您的前端代码连接到模拟器,而不是生产环境。打开public/js/homepage.js
文件,找到onDocumentReady
函数。我们可以看到代码访问了标准的 Firestore 和 Auth 实例:
公共/js/homepage.js
const auth = firebaseApp.auth();
const db = firebaseApp.firestore();
让我们更新db
和auth
对象以指向本地模拟器:
公共/js/homepage.js
const auth = firebaseApp.auth();
const db = firebaseApp.firestore();
// ADD THESE LINES
if (location.hostname === "127.0.0.1") {
console.log("127.0.0.1 detected!");
auth.useEmulator("http://127.0.0.1:9099");
db.useEmulator("127.0.0.1", 8080);
}
现在,当应用程序在本地计算机(由托管模拟器提供服务)上运行时,Firestore 客户端也指向本地模拟器而不是生产数据库。
打开模拟器界面
在 Web 浏览器中,导航到http://127.0.0.1:4000/ 。您应该会看到 Emulator Suite UI。
单击以查看 Firestore 模拟器的 UI。由于使用--import
标志导入的数据, items
集合已包含数据。
4. 运行应用
打开应用程序
在您的 Web 浏览器中,导航至http://127.0.0.1:5000 ,您应该会看到 Fire Store 在您的机器上本地运行!
使用应用程序
在主页上选择一个项目,然后单击添加到购物车。不幸的是,您会遇到以下错误:
让我们修复那个错误!因为一切都在模拟器中运行,我们可以进行实验而不用担心影响真实数据。
5.调试应用
找到错误
好的,让我们看看 Chrome 开发者控制台。按Control+Shift+J
(Windows、Linux、Chrome 操作系统)或Command+Option+J
(Mac) 查看控制台上的错误:
addToCart
方法似乎有一些错误,让我们看一下。我们在哪里尝试访问该方法中称为uid
东西,为什么它会是null
?现在,该方法在public/js/homepage.js
中看起来像这样:
公共/js/homepage.js
addToCart(id, itemData) {
console.log("addToCart", id, JSON.stringify(itemData));
return this.db
.collection("carts")
.doc(this.auth.currentUser.uid)
.collection("items")
.doc(id)
.set(itemData);
}
啊哈!我们没有登录该应用程序。根据Firebase 身份验证文档,当我们未登录时, auth.currentUser
为null
。让我们为此添加一个检查:
公共/js/homepage.js
addToCart(id, itemData) {
// ADD THESE LINES
if (this.auth.currentUser === null) {
this.showError("You must be signed in!");
return;
}
// ...
}
测试应用
现在,刷新页面,然后单击添加到购物车。这次你应该得到一个更好的错误:
但是,如果您单击上方工具栏中的登录,然后再次单击添加到购物车,您将看到购物车已更新。
但是,看起来这些数字根本不正确:
别担心,我们会尽快修复该错误。首先,让我们深入了解将商品添加到购物车时实际发生的情况。
6.局部函数触发器
单击“添加到购物车”会启动涉及多个仿真器的一系列事件。在 Firebase CLI 日志中,将商品添加到购物车后,您应该会看到类似于以下消息的内容:
i functions: Beginning execution of "calculateCart" i functions: Finished "calculateCart" in ~1s
发生了四个关键事件来生成这些日志和您观察到的 UI 更新:
1) Firestore 写入 - 客户端
Firestore 集合/carts/{cartId}/items/{itemId}/
中添加了一个新文档。您可以在public/js/homepage.js
addToCart
函数中看到这段代码:
公共/js/homepage.js
addToCart(id, itemData) {
// ...
console.log("addToCart", id, JSON.stringify(itemData));
return this.db
.collection("carts")
.doc(this.auth.currentUser.uid)
.collection("items")
.doc(id)
.set(itemData);
}
2)云函数触发
Cloud Function calculateCart
通过使用onWrite
触发器侦听发生在购物车项目上的任何写入事件(创建、更新或删除),您可以在functions/index.js
中看到:
函数/index.js
exports.calculateCart = functions.firestore
.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
try {
let totalPrice = 125.98;
let itemCount = 8;
const cartRef = db.collection("carts").doc(context.params.cartId);
await cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
}
);
3) Firestore 写入 - 管理员
calculateCart
函数读取购物车中的所有商品并将总数量和价格相加,然后用新的总计更新“购物车”文档(请参阅上面的cartRef.update(...)
)。
4) Firestore 读取 - 客户端
Web 前端已订阅以接收有关购物车更改的更新。在 Cloud Function 写入新的总计并更新 UI 后,它会获得实时更新,如您在public/js/homepage.js
中所见:
公共/js/homepage.js
this.cartUnsub = cartRef.onSnapshot(cart => {
// The cart document was changed, update the UI
// ...
});
回顾
干得好!您只需设置一个完全本地化的应用程序,它使用三个不同的 Firebase 模拟器进行完全本地化测试。
但是等等,还有更多!在下一节中,您将学习:
- 如何编写使用 Firebase 模拟器的单元测试。
- 如何使用 Firebase 模拟器调试您的安全规则。
7. 为您的应用创建安全规则
我们的网络应用程序读取和写入数据,但到目前为止我们还没有真正担心安全问题。 Cloud Firestore 使用名为“安全规则”的系统来声明谁有权读取和写入数据。 Emulator Suite 是制作这些规则原型的好方法。
在编辑器中,打开文件emulators-codelab/codelab-initial-state/firestore.rules
。您会看到我们的规则分为三个主要部分:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// User's cart metadata
match /carts/{cartID} {
// TODO: Change these! Anyone can read or write.
allow read, write: if true;
}
// Items inside the user's cart
match /carts/{cartID}/items/{itemID} {
// TODO: Change these! Anyone can read or write.
allow read, write: if true;
}
// All items available in the store. Users can read
// items but never write them.
match /items/{itemID} {
allow read: if true;
}
}
}
现在任何人都可以在我们的数据库中读取和写入数据!我们要确保只有有效的操作才能通过,并且我们不会泄露任何敏感信息。
在此 Codelab 中,遵循最小特权原则,我们将锁定所有文档并逐渐添加访问权限,直到所有用户都拥有所需的所有访问权限,但不会更多。让我们通过将条件设置为false
来更新前两个规则以拒绝访问:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// User's cart metadata
match /carts/{cartID} {
// UPDATE THIS LINE
allow read, write: if false;
}
// Items inside the user's cart
match /carts/{cartID}/items/{itemID} {
// UPDATE THIS LINE
allow read, write: if false;
}
// All items available in the store. Users can read
// items but never write them.
match /items/{itemID} {
allow read: if true;
}
}
}
8. 运行模拟器和测试
启动模拟器
在命令行上,确保您位于emulators-codelab/codelab-initial-state/
中。您可能仍在运行前面步骤中的模拟器。如果没有,请再次启动模拟器:
$ firebase emulators:start --import=./seed
模拟器运行后,您可以在本地针对它们运行测试。
运行测试
在目录emulators-codelab/codelab-initial-state/
的新终端选项卡中的命令行上
首先进入 functions 目录(我们将在此处完成 Codelab 的剩余部分):
$ cd functions
现在在 functions 目录中运行 mocha 测试,并滚动到输出的顶部:
# Run the tests $ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping carts 1) can be created and updated by the cart owner 2) can be read only by the cart owner shopping cart items 3) can be read only by the cart owner 4) can be added only by the cart owner adding an item to the cart recalculates the cart total. - should sum the cost of their items 0 passing (364ms) 1 pending 4 failing
现在我们有四个失败。在构建规则文件时,您可以通过观察更多测试的通过来衡量进度。
9. 安全购物车访问
前两个失败是“购物车”测试,用于测试:
- 用户只能创建和更新自己的购物车
- 用户只能读取自己的购物车
函数/test.js
it('can be created and updated by the cart owner', async () => {
// Alice can create her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").set({
ownerUID: "alice",
total: 0
}));
// Bob can't create Alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart").set({
ownerUID: "alice",
total: 0
}));
// Alice can update her own cart with a new total
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").update({
total: 1
}));
// Bob can't update Alice's cart with a new total
await firebase.assertFails(bobDb.doc("carts/alicesCart").update({
total: 1
}));
});
it("can be read only by the cart owner", async () => {
// Setup: Create Alice's cart as admin
await admin.doc("carts/alicesCart").set({
ownerUID: "alice",
total: 0
});
// Alice can read her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").get());
// Bob can't read Alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart").get());
});
让我们通过这些测试。在编辑器中,打开安全规则文件firestore.rules
并更新match /carts/{cartID}
中的语句:
firestore.规则
rules_version = '2';
service cloud.firestore {
// UPDATE THESE LINES
match /carts/{cartID} {
allow create: if request.auth.uid == request.resource.data.ownerUID;
allow read, update, delete: if request.auth.uid == resource.data.ownerUID;
}
// ...
}
}
这些规则现在只允许购物车所有者进行读写访问。
为了验证传入的数据和用户的身份验证,我们使用两个在每个规则的上下文中可用的对象:
-
request
对象包含有关正在尝试的操作的数据和元数据。 - 如果 Firebase 项目使用Firebase Authentication ,则
request.auth
对象描述发出请求的用户。
10.测试购物车访问
每当保存firestore.rules
时,Emulator Suite 会自动更新规则。您可以通过在运行模拟器的选项卡中查看消息Rules updated
来确认模拟器已更新规则:
重新运行测试,并检查前两个测试现在是否通过:
$ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping carts ✓ can be created and updated by the cart owner (195ms) ✓ can be read only by the cart owner (136ms) shopping cart items 1) can be read only by the cart owner 2) can be added only by the cart owner adding an item to the cart recalculates the cart total. - should sum the cost of their items 2 passing (482ms) 1 pending 2 failing
好工作!您现在已经获得了对购物车的访问权限。让我们继续下一个失败的测试。
11. 检查 UI 中的“添加到购物车”流程
现在,虽然购物车所有者可以读写他们的购物车,但他们无法读写购物车中的单个商品。这是因为虽然所有者可以访问购物车文档,但他们无权访问购物车的items 子集合。
这对用户来说是一种破碎的状态。
返回运行在http://127.0.0.1:5000,
并尝试向您的购物车中添加一些东西。您会收到一个Permission Denied
错误,从调试控制台可见,因为我们尚未授予用户访问items
子集合中创建的文档的权限。
12.允许购物车项目访问
这两个测试确认用户只能将商品添加到自己的购物车或从自己的购物车中读取商品:
it("can be read only by the cart owner", async () => {
// Alice can read items in her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart/items/milk").get());
// Bob can't read items in alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart/items/milk").get())
});
it("can be added only by the cart owner", async () => {
// Alice can add an item to her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart/items/lemon").set({
name: "lemon",
price: 0.99
}));
// Bob can't add an item to alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart/items/lemon").set({
name: "lemon",
price: 0.99
}));
});
因此,我们可以编写一个规则,如果当前用户的 UID 与购物车文档上的 ownerUID 相同,则允许访问。由于不需要为create, update, delete
指定不同的规则,您可以使用write
规则,该规则适用于所有修改数据的请求。
更新 items 子集合中文档的规则。条件中的get
是从 Firestore 中读取一个值——在本例中,是购物车文档中的ownerUID
。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ...
// UPDATE THESE LINES
match /carts/{cartID}/items/{itemID} {
allow read, write: if get(/databases/$(database)/documents/carts/$(cartID)).data.ownerUID == request.auth.uid;
}
// ...
}
}
13.测试购物车物品访问
现在我们可以重新运行测试。滚动到输出顶部并检查是否有更多测试通过:
$ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping carts ✓ can be created and updated by the cart owner (195ms) ✓ can be read only by the cart owner (136ms) shopping cart items ✓ can be read only by the cart owner (111ms) ✓ can be added only by the cart owner adding an item to the cart recalculates the cart total. - should sum the cost of their items 4 passing (401ms) 1 pending
好的!现在我们所有的测试都通过了。我们有一项待定测试,但我们将分几步完成。
14.再次检查“添加到购物车”流程
返回到 Web 前端 ( http://127.0.0.1:5000 ) 并将商品添加到购物车。这是确认我们的测试和规则是否符合客户所需功能的重要步骤。 (请记住,我们上次试用 UI 时用户无法将商品添加到他们的购物车!)
保存firestore.rules
时,客户端会自动重新加载规则。因此,尝试向购物车中添加一些东西。
回顾
干得好!您刚刚提高了应用程序的安全性,这是为生产做好准备的重要一步!如果这是一个生产应用程序,我们可以将这些测试添加到我们的持续集成管道中。这将使我们有信心继续前进,即我们的购物车数据将具有这些访问控制,即使其他人正在修改规则。
但是等等,还有更多!
如果你继续你会学到:
- 如何编写由 Firestore 事件触发的函数
- 如何创建适用于多个模拟器的测试
15. 设置 Cloud Functions 测试
到目前为止,我们一直专注于 Web 应用程序的前端和 Firestore 安全规则。但此应用程序还使用 Cloud Functions 来使用户的购物车保持最新状态,因此我们也想测试该代码。
Emulator Suite 让测试 Cloud Functions 变得如此简单,甚至是使用 Cloud Firestore 和其他服务的函数。
在编辑器中,打开emulators-codelab/codelab-initial-state/functions/test.js
文件并滚动到文件中的最后一个测试。现在,它被标记为待处理:
// REMOVE .skip FROM THIS LINE
describe.skip("adding an item to the cart recalculates the cart total. ", () => {
// ...
it("should sum the cost of their items", async () => {
...
});
});
要启用测试,请删除.skip
,因此它看起来像这样:
describe("adding an item to the cart recalculates the cart total. ", () => {
// ...
it("should sum the cost of their items", async () => {
...
});
});
接下来,找到文件顶部的REAL_FIREBASE_PROJECT_ID
变量并将其更改为您的真实 Firebase 项目 ID。:
// CHANGE THIS LINE
const REAL_FIREBASE_PROJECT_ID = "changeme";
如果您忘记了您的项目 ID,您可以在 Firebase 控制台的项目设置中找到您的 Firebase 项目 ID:
16. 演练功能测试
由于此测试验证了 Cloud Firestore 和 Cloud Functions 之间的交互,因此它比之前的代码实验室中的测试涉及更多设置。让我们浏览一下这个测试并了解它的期望。
创建购物车
Cloud Functions 在受信任的服务器环境中运行,可以使用 Admin SDK 使用的服务帐户身份验证。首先,您使用initializeAdminApp
而不是initializeApp
来初始化应用程序。然后,您为我们将向其添加项目的购物车创建一个DocumentReference并初始化购物车:
it("should sum the cost of their items", async () => {
const db = firebase
.initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
.firestore();
// Setup: Initialize cart
const aliceCartRef = db.doc("carts/alice")
await aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });
...
});
触发功能
然后,将文档添加到购物车文档的items
子集合中以触发该功能。添加两项以确保您正在测试函数中发生的添加。
it("should sum the cost of their items", async () => {
const db = firebase
.initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
.firestore();
// Setup: Initialize cart
const aliceCartRef = db.doc("carts/alice")
await aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });
// Trigger calculateCart by adding items to the cart
const aliceItemsRef = aliceCartRef.collection("items");
await aliceItemsRef.doc("doc1").set({name: "nectarine", price: 2.99});
await aliceItemsRef.doc("doc2").set({ name: "grapefruit", price: 6.99 });
...
});
});
设定测试期望
使用onSnapshot()
为购物车文档上的任何更改注册一个侦听器。 onSnapshot()
返回一个函数,您可以调用该函数来注销侦听器。
对于此测试,添加两个总价为 9.98 美元的项目。然后,检查购物车是否具有预期的itemCount
和totalPrice
。如果是这样,那么该函数就完成了它的工作。
it("should sum the cost of their items", (done) => {
const db = firebase
.initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
.firestore();
// Setup: Initialize cart
const aliceCartRef = db.doc("carts/alice")
aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });
// Trigger calculateCart by adding items to the cart
const aliceItemsRef = aliceCartRef.collection("items");
aliceItemsRef.doc("doc1").set({name: "nectarine", price: 2.99});
aliceItemsRef.doc("doc2").set({ name: "grapefruit", price: 6.99 });
// Listen for every update to the cart. Every time an item is added to
// the cart's subcollection of items, the function updates `totalPrice`
// and `itemCount` attributes on the cart.
// Returns a function that can be called to unsubscribe the listener.
await new Promise((resolve) => {
const unsubscribe = aliceCartRef.onSnapshot(snap => {
// If the function worked, these will be cart's final attributes.
const expectedCount = 2;
const expectedTotal = 9.98;
// When the `itemCount`and `totalPrice` match the expectations for the
// two items added, the promise resolves, and the test passes.
if (snap.data().itemCount === expectedCount && snap.data().totalPrice == expectedTotal) {
// Call the function returned by `onSnapshot` to unsubscribe from updates
unsubscribe();
resolve();
};
});
});
});
});
17. 运行测试
您可能仍在运行之前测试的模拟器。如果没有,请启动模拟器。从命令行运行
$ firebase emulators:start --import=./seed
打开一个新的终端选项卡(让模拟器保持运行)并进入功能目录。您可能仍然从安全规则测试中打开它。
$ cd functions
现在运行单元测试,你应该看到总共 5 个测试:
$ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping cart creation ✓ can be created by the cart owner (82ms) shopping cart reads, updates, and deletes ✓ cart can be read by the cart owner (42ms) shopping cart items ✓ items can be read by the cart owner (40ms) ✓ items can be added by the cart owner adding an item to the cart recalculates the cart total. 1) should sum the cost of their items 4 passing (2s) 1 failing
如果查看具体故障,似乎是超时错误。这是因为测试正在等待函数正确更新,但它从未这样做过。现在,我们准备编写函数来满足测试。
18.写一个函数
要修复此测试,您需要更新functions/index.js
中的函数。尽管已编写此功能的一部分,但还不完整。这是函数当前的样子:
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
let totalPrice = 125.98;
let itemCount = 8;
try {
const cartRef = db.collection("carts").doc(context.params.cartId);
await cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
});
该函数正确设置了购物车引用,但随后没有计算totalPrice
和itemCount
的值,而是将它们更新为硬编码值。
获取并遍历
items
子集
将新常量itemsSnap
初始化为items
子集合。然后,遍历集合中的所有文档。
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
try {
let totalPrice = 125.98;
let itemCount = 8;
const cartRef = db.collection("carts").doc(context.params.cartId);
// ADD LINES FROM HERE
const itemsSnap = await cartRef.collection("items").get();
itemsSnap.docs.forEach(item => {
const itemData = item.data();
})
// TO HERE
return cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
});
计算 totalPrice 和 itemCount
首先,让我们将totalPrice
和itemCount
的值初始化为零。
然后,将逻辑添加到我们的迭代块中。首先,检查商品是否有价格。如果该项目没有指定数量,让它默认为1
。然后,将数量添加到itemCount
的运行总计中。最后,将项目的价格乘以数量添加到totalPrice
的运行总计中:
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
try {
// CHANGE THESE LINES
let totalPrice = 0;
let itemCount = 0;
const cartRef = db.collection("carts").doc(context.params.cartId);
const itemsSnap = await cartRef.collection("items").get();
itemsSnap.docs.forEach(item => {
const itemData = item.data();
// ADD LINES FROM HERE
if (itemData.price) {
// If not specified, the quantity is 1
const quantity = itemData.quantity ? itemData.quantity : 1;
itemCount += quantity;
totalPrice += (itemData.price * quantity);
}
// TO HERE
})
await cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
});
您还可以添加日志记录以帮助调试成功和错误状态:
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
let totalPrice = 0;
let itemCount = 0;
try {
const cartRef = db.collection("carts").doc(context.params.cartId);
const itemsSnap = await cartRef.collection("items").get();
itemsSnap.docs.forEach(item => {
const itemData = item.data();
if (itemData.price) {
// If not specified, the quantity is 1
const quantity = (itemData.quantity) ? itemData.quantity : 1;
itemCount += quantity;
totalPrice += (itemData.price * quantity);
}
});
await cartRef.update({
totalPrice,
itemCount
});
// OPTIONAL LOGGING HERE
console.log("Cart total successfully recalculated: ", totalPrice);
} catch(err) {
// OPTIONAL LOGGING HERE
console.warn("update error", err);
}
});
19. 重新运行测试
在命令行上,确保模拟器仍在运行并重新运行测试。您不需要重新启动模拟器,因为它们会自动获取对功能的更改。您应该看到所有测试都通过了:
$ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping cart creation ✓ can be created by the cart owner (306ms) shopping cart reads, updates, and deletes ✓ cart can be read by the cart owner (59ms) shopping cart items ✓ items can be read by the cart owner ✓ items can be added by the cart owner adding an item to the cart recalculates the cart total. ✓ should sum the cost of their items (800ms) 5 passing (1s)
好工作!
20. 使用 Storefront UI 进行尝试
对于最终测试,返回到 Web 应用程序 ( http://127.0.0.1:5000/ ) 并将商品添加到购物车。
确认购物车更新了正确的总数。极好的!
回顾
您已经完成了 Cloud Functions for Firebase 和 Cloud Firestore 之间的复杂测试用例。您编写了一个 Cloud Function 来使测试通过。您还确认新功能在 UI 中正常运行!您在本地完成所有这些操作,在您自己的机器上运行模拟器。
您还创建了一个针对本地模拟器运行的 Web 客户端,定制了安全规则来保护数据,并使用本地模拟器测试了安全规则。