使用 Firebase 模拟器套件进行本地开发

1. 开始之前

Cloud Firestore 和 Cloud Functions 等无服务器后端工具非常易于使用,但可能难以测试。 Firebase 本地模拟器套件允许您在开发机器上运行这些服务的本地版本,以便您可以快速安全地开发应用程序。

先决条件

  • 一个简单的编辑器,例如 Visual Studio Code、Atom 或 Sublime Text
  • Node.js的10.0.0或更高版本(安装Node.js的,用NVM ,检查您的版本,运行node --version
  • Java的7或更高版本(安装Java使用这些指令,检查您的版本,运行java -version

你会做什么

在此 Codelab 中,您将运行和调试一个简单的在线购物应用,该应用由多个 Firebase 服务提供支持:

  • 云公司的FireStore:全球可扩展性,无服务器的NoSQL数据库的实时能力。
  • 云功能:无服务器后端代码,在响应事件或HTTP请求运行。
  • 火力地堡认证:一个管理的认证服务,与其他火力地堡产品集成。
  • 火力地堡托管:快捷,安全的托管Web应用程序。

您将应用程序连接到模拟器套件以启用本地开发。

2589e2f95b74fa88.png

您还将学习如何:

  • 如何将您的应用程序连接到模拟器套件以及各种模拟器是如何连接的。
  • Firebase 安全规则的工作原理以及如何针对本地模拟器测试 Firestore 安全规则。
  • 如何编写由 Firestore 事件触发的 Firebase 函数,以及如何编写针对模拟器套件运行的集成测试。

2. 设置

获取源代码

在这个 codelab 中,您从一个几乎完整的 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 项目

如果你没有一个火力地堡项目,在火力地堡控制台,创建一个新的火力地堡项目。记下您选择的项目 ID,稍后您将需要它。

现在我们需要将此代码连接到您的 Firebase 项目。首先运行以下命令登录 Firebase CLI:

$ firebase login

接下来运行以下命令以创建项目别名。替换$YOUR_PROJECT_ID与火力地堡项目的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://localhost: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://localhost:4000                │
└─────────────────────────────────────────────────────────────┘

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ localhost:5001 │ http://localhost:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ localhost:8080 │ http://localhost:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting        │ localhost:5000 │ n/a                             │
└────────────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at localhost:4400
  Other reserved ports: 4500

Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.

一旦你开始看到消息的所有仿真器,应用程序就可以使用了。

将 Web 应用程序连接到模拟器

基于在日志上表中我们可以看出,云计算公司的FireStore仿真器侦听端口8080和验证仿真器侦听端口9099

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ localhost:5001 │ http://localhost:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ localhost:8080 │ http://localhost:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting        │ localhost:5000 │ n/a                             │
└────────────────┴────────────────┴─────────────────────────────────┘

让我们将您的前端代码连接到模拟器,而不是生产环境。打开public/js/homepage.js文件并找到onDocumentReady功能。我们可以看到代码访问了标准的 Firestore 和 Auth 实例:

公共/js/homepage.js

  const auth = firebaseApp.auth();
  const db = firebaseApp.firestore();

让我们更新的dbauth对象指向本地模拟器:

公共/js/homepage.js

  const auth = firebaseApp.auth();
  const db = firebaseApp.firestore();

  // ADD THESE LINES
  if (location.hostname === "localhost") {
    console.log("localhost detected!");
    auth.useEmulator("http://localhost:9099");
    db.useEmulator("localhost", 8080);
  }

现在,当应用在 localhost(由托管模拟器提供服务)上运行时,Firestore 客户端也指向本地模拟器而不是生产数据库。

打开模拟器界面

在Web浏览器,浏览到HTTP://本地主机:4000 / 。您应该会看到 Emulator Suite UI。

模拟器 UI 主屏幕

单击以查看 Firestore 模拟器的 UI。该items集已经包含因为与导入的数据的数据--import标志。

4ef88d0148405d36.png

4. 运行应用

打开应用程序

在Web浏览器,浏览到HTTP://本地主机:5000 ,你应该看到消防存储你的机器上本地运行的!

939f87946bac2ee4.png

使用应用程序

匹克在主页上的信息并点击添加到购物车。不幸的是,您将遇到以下错误:

a11bd59933a8e885.png

让我们修复那个错误!因为一切都在模拟器中运行,我们可以进行实验,而不必担心影响真实数据。

5. 调试应用

找到错误

好的,让我们看看 Chrome 开发者控制台。按下Control+Shift+J (Windows,Linux和Chrome操作系统)或Command+Option+J (苹果机)看到控制台上的错误:

74c45df55291dab1.png

好像有在一些错误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);
  }

啊哈!我们没有登录应用程序。根据该火力地堡认证文档,当我们还没有登录, auth.currentUsernull 。让我们为此添加一个检查:

公共/js/homepage.js

  addToCart(id, itemData) {
    // ADD THESE LINES
    if (this.auth.currentUser === null) {
      this.showError("You must be signed in!");
      return;
    }

    // ...
  }

测试应用

现在,刷新页面,然后单击添加到购物车。这次你应该得到一个更好的错误:

c65f6c05588133f7.png

但是,如果你上工具栏上单击登录,然后单击添加到购物车再次,你会看到车被更新。

但是,这些数字看起来根本不正确:

239f26f02f959eef.png

别担心,我们会尽快修复这个错误。首先,让我们深入了解将商品添加到购物车时实际发生的情况。

6. 本地函数触发器

点击添加到购物车揭开序幕,涉及多个仿真器的事件链。在 Firebase CLI 日志中,将商品添加到购物车后,您应该会看到类似以下消息:

i  functions: Beginning execution of "calculateCart"
i  functions: Finished "calculateCart" in ~1s

产生这些日志和您观察到的 UI 更新发生了四个关键事件:

68c9323f2ad10f7a.png

1) Firestore 写入 - 客户端

一个新的文档添加到公司的FireStore采集/carts/{cartId}/items/{itemId}/ 。你可以看到在这个代码addToCart内部功能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);
  }

2)云函数触发

云功能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 前端以接收有关购物车更改的更新。它得到实时更新的云功能写入新的总数,并更新用户界面,你可以在看后public/js/homepage.js

公共/js/homepage.js

this.cartUnsub = cartRef.onSnapshot(cart => {
   // The cart document was changed, update the UI
   // ...
});

回顾

干得好!您只需设置一个完全本地化的应用程序,该应用程序使用三个不同的 Firebase 模拟器进行完全本地化测试。

db82eef1706c9058.gif

但是等等,还有更多!在下一节中,您将学习:

  • 如何编写使用 Firebase 模拟器的单元测试。
  • 如何使用 Firebase 模拟器调试您的安全规则。

7. 为您的应用量身定制安全规则

我们的 Web 应用程序读取和写入数据,但到目前为止我们根本没有真正担心安全性。 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/

首先进入函数目录(我们将在此代码实验室的其余部分停留):

$ cd 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

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;
    }

    // ...
  }
}

这些规则现在只允许购物车所有者进行读写访问。

为了验证传入数据和用户的身份验证,我们使用每个规则上下文中可用的两个对象:

10. 测试购物车访问

该仿真器套件自动更新每当规则firestore.rules被保存。您可以确认的是,仿真器具备通过查看运行的消息仿真器选项卡中更新的规则Rules updated

5680da418b420226.png

重新运行测试,并检查前两个测试现在是否通过:

$ 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 中的“添加到购物车”流程

目前,虽然购物车所有者可以读写他们的购物车,但他们无法读取或写入购物车中的单个项目。这是因为当业主有机会获得车文档,他们没有进入购物车的物品子集合

这对用户来说是一个破碎的状态。

返回到Web UI,其上运行http://localhost: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规则,它适用于所有的请求修改数据。

更新项目子集合中文档的规则。该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://本地主机:5000 )和添加项目到购物车。这是确认我们的测试和规则与客户所需的功能相匹配的重要步骤。 (请记住,上次我们试用 UI 时,用户无法将商品添加到他们的购物车!)

69ad26cee520bf24.png

当客户端会自动重新加载规则firestore.rules被保存。因此,尝试将一些东西添加到购物车。

回顾

干得好!您刚刚提高了应用程序的安全性,这是为生产做好准备的重要一步!如果这是一个生产应用程序,我们可以将这些测试添加到我们的持续集成管道中。这将使我们相信我们的购物车数据将具有这些访问控制,即使其他人正在修改规则。

ba5440b193e75967.gif

但是等等,还有更多!

如果你继续,你会学到:

  • 如何编写由 Firestore 事件触发的函数
  • 如何创建跨多个模拟器工作的测试

15. 设置 Cloud Functions 测试

到目前为止,我们一直专注于 Web 应用程序的前端和 Firestore 安全规则。但此应用程序还使用 Cloud Functions 来使用户的购物车保持最新状态,因此我们也想测试该代码。

模拟器套件使测试 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在文件的顶部变量,将其更改为你的真实火力地堡项目ID:

// CHANGE THIS LINE
const REAL_FIREBASE_PROJECT_ID = "changeme";

如果您忘记了项目 ID,可以在 Firebase 控制台的项目设置中找到您的 Firebase 项目 ID:

d6d0429b700d2b21.png

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 美元。然后,检查车中有预期itemCounttotalPrice 。如果是这样,那么该函数就完成了它的工作。

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

该功能正确设置车的参考,但随后而不是计算的值totalPriceitemCount ,它更新他们对硬编码的。

获取并迭代

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

首先,让我们来初始化值totalPriceitemCount为零。

然后,将逻辑添加到我们的迭代块中。首先,检查商品是否有价格。如果没有指定的项目确实数量,让它默认为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://本地主机:5000 / ),并添加项目到购物车。

69ad26cee520bf24.png

确认购物车更新了正确的总数。极好的!

回顾

您已经了解了 Cloud Functions for Firebase 和 Cloud Firestore 之间的复杂测试用例。您编写了一个 Cloud Functions 函数来使测试通过。您还确认新功能在 UI 中有效!您在本地完成所有这些操作,在您自己的机器上运行模拟器。

您还创建了一个针对本地模拟器运行的 Web 客户端,定制了安全规则来保护数据,并使用本地模拟器测试了安全规则。

c6a7aeb91fe97a64.gif