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 Hosting:為網路應用程式提供快速又安全的託管服務。
您會將應用程式連線至 Emulator Suite,以啟用本機開發。
您也會瞭解如何:
- 如何將應用程式連線至 Emulator Suite,以及如何連線至各種模擬器。
- Firebase 安全性規則的運作方式,以及如何針對本機模擬器測試 Firestore 安全性規則。
- 如何編寫由 Firestore 事件觸發的 Firebase 函式,以及如何編寫針對 Emulator Suite 執行的整合測試。
2. 設定
取得原始碼
在本程式碼研究室中,您會從近乎完成的 The 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
模擬器套件是 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 選項。
- 按一下「建立專案」,等待專案佈建完成,然後按一下「繼續」。
將程式碼連結至 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/。您應該會看到模擬器套件使用者介面。
按一下即可查看 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;
}
// ...
}
測試應用程式
現在請重新整理頁面,然後按一下「加入購物車」。這次應該會收到更友善的錯誤訊息:
但如果您點選上方工具列中的「登入」,然後再次點選「加入購物車」,就會發現購物車已更新。
不過,這些數字似乎完全不正確:
請放心,我們很快就會修正這個錯誤。首先,讓我們深入瞭解將商品加入購物車時實際發生的情況。
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
函式中看到這段程式碼:
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 Functions
Cloud Function 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 Function 寫入新的總計值並更新 UI 後,系統會即時更新,如 public/js/homepage.js
所示:
public/js/homepage.js
this.cartUnsub = cartRef.onSnapshot(cart => {
// The cart document was changed, update the UI
// ...
});
重點回顧
做得好!您剛設定的應用程式完全在本機執行,並使用三種不同的 Firebase 模擬器進行本機測試。
好處可不只這些!下一個章節將說明:
- 如何編寫使用 Firebase Emulator 的單元測試。
- 如何使用 Firebase 模擬器偵錯安全性規則。
7. 為應用程式量身打造安全性規則
我們的 Web 應用程式會讀取及寫入資料,但目前為止,我們完全不擔心安全性。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. 在 UI 中檢查「新增至購物車」流程
目前購物車擁有者可以讀取及寫入購物車,但無法讀取或寫入購物車中的個別商品。這是因為擁有者可以存取購物車文件,但無法存取購物車的項目子集合。
這會導致使用者無法正常運作。
返回 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
}));
});
因此,我們可以編寫規則,允許目前使用者存取購物車文件,前提是使用者具有與 ownerUID 相同的 UID。由於不需要為 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. 再次檢查「加入購物車」流程
返回網頁前端 ( http://127.0.0.1:5000),然後將商品加入購物車。這是重要步驟,可確認我們的測試和規則符合客戶所需的功能。(請注意,上次我們試用 UI 時,使用者無法將商品加入購物車!)
儲存 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
開啟新的終端機分頁 (讓模擬器保持執行狀態),然後移至函式目錄。您可能仍會開啟這個視窗,以便進行安全性規則測試。
$ 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. 使用店面使用者介面試試看
最後一項測試:返回網頁應用程式 ( http://127.0.0.1:5000/),然後將商品加入購物車。
確認購物車會更新為正確的總金額。太棒了!
重點回顧
您已逐步完成 Cloud Functions for Firebase 和 Cloud Firestore 之間的複雜測試案例。您編寫了 Cloud 函式,讓測試通過。您也確認了新功能在 UI 中運作正常!您在本機完成所有作業,並在自己的電腦上執行模擬器。
您也建立了針對本機模擬器執行的網路用戶端、量身打造的安全性規則來保護資料,並使用本機模擬器測試安全性規則。