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)
學習內容
在本程式碼實驗室中,您將執行及偵錯一個簡單的線上購物應用程式,該應用程式由多項 Firebase 服務提供支援:
- Cloud Firestore:可全球擴充的無伺服器 NoSQL 資料庫,具備即時功能。
- Cloud Functions:無伺服器後端程式碼,可根據事件或 HTTP 要求執行。
- Firebase 驗證:與其他 Firebase 產品整合的代管驗證服務。
- Firebase 託管:快速安全的網頁應用程式託管服務。
您會將應用程式連結至模擬器套件,以便在本機開發。

您也會瞭解如何:
- 如何將應用程式連結至 Emulator Suite,以及各種模擬器的連結方式。
- Firebase 安全規則的運作方式,以及如何在本機模擬器測試 Firestore 安全規則。
- 如何編寫由 Firestore 事件觸發的 Firebase 函式,以及如何編寫針對模擬器套件執行的整合測試。
2. 設定
取得原始碼
在本程式碼實驗室中,您將從幾乎完成的 Fire Store 範例版本開始,因此首先需要複製原始碼:
$ git clone https://github.com/firebase/emulators-codelab.git
接著,切換至程式碼實驗室目錄,您將在該目錄中完成本程式碼實驗室的其餘部分:
$ 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。本程式碼實驗室應適用於 9.0.0 以上版本,但後續版本包含更多錯誤修正。
$ firebase --version 9.6.0
連結至 Firebase 專案
建立 Firebase 專案
- 使用 Google 帳戶登入 Firebase 控制台。
- 按一下按鈕建立新專案,然後輸入專案名稱 (例如
Emulators Codelab)。
- 按一下「繼續」。
- 如果出現提示訊息,請詳閱並接受 Firebase 條款,然後點選「繼續」。
- (選用) 在 Firebase 控制台中啟用 AI 輔助功能 (稱為「Gemini in Firebase」)。
- 在本程式碼實驗室中,您不需要 Google Analytics,因此請關閉 Google Analytics 選項。
- 按一下「Create project」,等待專案佈建完成,然後點選「Continue」。
將程式碼連結至 Firebase 專案
現在,我們需要將這段程式碼連結至 Firebase 專案。首先,執行下列指令,登入 Firebase CLI:
$ firebase login
接著執行下列指令,建立專案別名。將 $YOUR_PROJECT_ID 替換為 Firebase 專案 ID。
$ firebase use $YOUR_PROJECT_ID
現在可以執行應用程式了!
3. 執行模擬器
在本節中,您將在本機執行應用程式。這表示可以啟動 Emulator Suite 了。
啟動模擬器
在程式碼實驗室來源目錄中,執行下列指令來啟動模擬器:
$ 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.
看到「All emulators started」訊息後,即可開始使用應用程式。
將網頁應用程式連結至模擬器
根據記錄中的表格,Cloud Firestore 模擬器會監聽通訊埠 8080,Authentication 模擬器則會監聽通訊埠 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 執行個體:
public/js/homepage.js
const auth = firebaseApp.auth();
const db = firebaseApp.firestore();
請更新 db 和 auth 物件,指向本機模擬器:
public/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);
}
現在,當應用程式在本機電腦上執行 (由 Hosting 模擬器提供) 時,Firestore 用戶端也會指向本機模擬器,而非實際工作環境資料庫。
開啟 EmulatorUI
在網路瀏覽器中前往 http://127.0.0.1:4000/。您應該會看到 Emulator Suite UI。

按一下即可查看 Firestore 模擬器的 UI。由於使用 --import 旗標匯入資料,items 集合已包含資料。

4. 執行應用程式
開啟應用程式
在網路瀏覽器中前往 http://127.0.0.1:5000,應該會看到 The Fire Store 在本機電腦上執行!

使用應用程式
在首頁挑選一項商品,然後按一下「加入購物車」。很抱歉,您會遇到下列錯誤:

讓我們一起修正這個錯誤!由於所有內容都在模擬器中執行,因此我們可以進行實驗,不必擔心影響實際資料。
5. 對應用程式進行偵錯
找出錯誤
好的,我們來看看 Chrome 開發人員控制台。按下 Control+Shift+J (Windows、Linux、ChromeOS) 或 Command+Option+J (Mac),即可在控制台中查看錯誤:

addToCart 方法似乎發生錯誤,我們來看看。我們在該方法中嘗試存取名為 uid 的內容,為什麼會是 null?目前,public/js/homepage.js 中的方法如下所示:
public/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。我們來新增檢查:
public/js/homepage.js
addToCart(id, itemData) {
// ADD THESE LINES
if (this.auth.currentUser === null) {
this.showError("You must be signed in!");
return;
}
// ...
}
測試應用程式
現在,請重新整理頁面,然後按一下「Add to Cart」。這次應該會收到更清楚的錯誤訊息:

不過,如果點選上方工具列中的「Sign In」,然後再次點選「Add to Cart」,就會看到購物車已更新。
不過,這些數字似乎完全不正確:

別擔心,我們很快就會修正這個錯誤。首先,我們來深入瞭解將商品加入購物車時,實際發生的情況。
6. 本機函式觸發條件
點選「Add to Cart」會啟動一連串事件,涉及多個模擬器。將商品加入購物車後,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 函式中查看這段程式碼:
public/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 函式
Cloud 函式 calculateCart 會使用 onWrite 觸發條件,監聽購物車商品發生的任何寫入事件 (建立、更新或刪除),您可以在 functions/index.js 中查看:
functions/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 讀取 - 用戶端
網頁前端會訂閱購物車異動通知。Cloud 函式寫入新的總計並更新 UI 後,就會取得即時更新,如 public/js/homepage.js 所示:
public/js/homepage.js
this.cartUnsub = cartRef.onSnapshot(cart => {
// The cart document was changed, update the UI
// ...
});
重點回顧
做得好!您剛剛設定了完全本機的應用程式,並使用三種不同的 Firebase 模擬器進行完全本機測試。

等一下,好處可不只這些!下一節將說明:
- 如何編寫使用 Firebase 模擬器的單元測試。
- 如何使用 Firebase 模擬器對安全性規則進行偵錯。
7. 為應用程式建立專屬安全性規則
我們的網頁應用程式會讀取及寫入資料,但到目前為止,我們完全不擔心安全性問題。Cloud Firestore 使用名為「安全性規則」的系統,宣告哪些使用者有權讀取及寫入資料。模擬器套件是建立這些規則原型的絕佳方式。
在編輯器中開啟 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;
}
}
}
目前任何人都可以讀取及寫入資料庫中的資料!我們希望確保只有有效的作業能通過,且不會洩漏任何機密資訊。
在本程式碼實驗室中,我們會遵循最小權限原則,鎖定所有文件並逐步新增存取權,直到所有使用者都取得所需權限,但不會超過。請將條件設為 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 目錄 (在程式碼實驗室的其餘部分,我們都會留在此處):
$ 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. 安全存取購物車
前兩項失敗的是「購物車」測試,測試內容如下:
- 使用者只能建立及更新自己的購物車
- 使用者只能讀取自己的購物車
functions/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;
}
// ...
}
}
現在,這些規則只允許購物車擁有者讀取及寫入資料。
為了驗證傳入資料和使用者驗證,我們使用每個規則內容中可用的兩個物件:
request物件包含嘗試執行的作業相關資料和中繼資料。- 如果 Firebase 專案使用 Firebase 驗證,
request.auth物件會描述提出要求的使用者。
10. 測試購物車存取權
每當 firestore.rules 儲存時,模擬器套件 都會自動更新規則。如要確認模擬器已更新規則,請在執行模擬器的分頁中查看 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. 在使用者介面中查看「加入購物車」流程
目前,雖然購物車擁有者可以讀取及寫入購物車,但無法讀取或寫入購物車中的個別商品。這是因為擁有者雖然可以存取購物車文件,但無法存取購物車的項目子集合。
這會導致使用者無法正常使用。
返回在 http://127.0.0.1:5000, 執行的網頁 UI,嘗試將商品加入購物車。您會收到 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. 再次檢查「加入購物車」流程
返回網頁前端 (http://127.0.0.1:5000),將商品加入購物車。這個步驟非常重要,可確認測試和規則是否符合客戶所需的功能。(請注意,上次我們測試使用者介面時,使用者無法將商品加入購物車!)

儲存 firestore.rules 後,用戶端會自動重新載入規則。試著將商品加入購物車。
重點回顧
做得好!您剛剛提升了應用程式的安全性,這是準備推出正式版的重要步驟!如果這是正式版應用程式,我們可以將這些測試新增至持續整合管道。這樣一來,即使其他人修改規則,我們也能確信購物車資料會受到這些存取控管機制的保護。

等一下,好處可不只這些!
如果繼續閱讀,您將瞭解:
- 如何編寫由 Firestore 事件觸發的函式
- 如何建立可在多個模擬器上執行的測試
15. 設定 Cloud Functions 測試
到目前為止,我們著重於網頁應用程式的前端和 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 變數,並變更為實際的 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
開啟新的終端機分頁 (讓模擬器繼續執行),然後前往 functions 目錄。您可能在測試安全性規則時就已開啟這個頁面。
$ 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 試用
最後,請返回網頁應用程式 (http://127.0.0.1:5000/),將商品加入購物車。

確認購物車更新後的總金額正確無誤。太棒了!
重點回顧
您已完成 Cloud Functions for Firebase 和 Cloud Firestore 之間的複雜測試案例。您編寫了 Cloud 函式,讓測試通過。您也確認了新功能在 UI 中正常運作!您在本機完成所有作業,並在自己的機器上執行模擬器。
您也建立了針對本機模擬器執行的網頁用戶端、自訂安全性規則來保護資料,並使用本機模擬器測試安全性規則。
