1. Hinweis
Serverlose Backend-Tools wie Cloud Firestore und Cloud Functions sind sehr einfach zu verwenden, aber schwer zu testen. Mit der Firebase Local Emulator Suite können Sie lokale Versionen dieser Dienste auf Ihrem Entwicklungscomputer ausführen, um Ihre App schnell und sicher zu entwickeln.
Voraussetzungen
- Einen einfachen Editor wie Visual Studio Code, Atom oder Sublime Text
- Node.js 10.0.0 oder höher (zum Installieren von Node.js verwenden Sie nvm, um Ihre Version zu prüfen, führen Sie
node --version
aus) - Java 7 oder höher. Folgen Sie dieser Anleitung, um Java zu installieren. Um Ihre Version zu prüfen, führen Sie
java -version
aus.
Aufgabe
In diesem Codelab führen Sie eine einfache Online-Shopping-App aus, die von mehreren Firebase-Diensten unterstützt wird, und beheben mögliche Fehler:
- Cloud Firestore: eine global skalierbare, serverlose NoSQL-Datenbank mit Echtzeitfunktionen.
- Cloud Functions: Serverloser Backend-Code, der als Reaktion auf Ereignisse oder HTTP-Anfragen ausgeführt wird.
- Firebase Authentication: Ein verwalteter Authentifizierungsdienst, der sich in andere Firebase-Produkte einbinden lässt.
- Firebase Hosting: schnelles und sicheres Hosting für Web-Apps
Sie stellen eine Verbindung zwischen der App und der Emulator Suite her, um die lokale Entwicklung zu ermöglichen.
Außerdem erfahren Sie, wie Sie:
- Wie Sie Ihre App mit der Emulator Suite verbinden und wie die verschiedenen Emulatoren verbunden sind.
- Funktionsweise von Firebase-Sicherheitsregeln und Testen von Firestore-Sicherheitsregeln mit einem lokalen Emulator
- Hier erfahren Sie, wie Sie eine Firebase-Funktion schreiben, die durch Firestore-Ereignisse ausgelöst wird, und wie Sie Integrationstests schreiben, die in der Emulator Suite ausgeführt werden.
2. Einrichten
Quellcode abrufen
In diesem Codelab beginnen Sie mit einer Version des Fire Store-Beispiels, die fast fertig ist. Klonen Sie also zuerst den Quellcode:
$ git clone https://github.com/firebase/emulators-codelab.git
Wechseln Sie dann zum Codelab-Verzeichnis, in dem Sie den Rest dieses Codelabs bearbeiten werden:
$ cd emulators-codelab/codelab-initial-state
Installieren Sie nun die Abhängigkeiten, damit Sie den Code ausführen können. Bei einer langsameren Internetverbindung kann dies ein bis zwei Minuten dauern:
# Move into the functions directory
$ cd functions
# Install dependencies
$ npm install
# Move back into the previous directory
$ cd ../
Firebase CLI abrufen
Die Emulator Suite ist Teil der Firebase CLI (Befehlszeilenschnittstelle), die mit dem folgenden Befehl auf Ihrem Computer installiert werden kann:
$ npm install -g firebase-tools
Prüfen Sie als Nächstes, ob Sie die neueste Version der Befehlszeile haben. Dieses Codelab sollte mit Version 9.0.0 oder höher funktionieren, spätere Versionen enthalten jedoch weitere Fehlerkorrekturen.
$ firebase --version 9.6.0
Verbindung mit Ihrem Firebase-Projekt herstellen
Wenn Sie noch kein Firebase-Projekt haben, erstellen Sie in der Firebase Console ein neues Firebase-Projekt. Notieren Sie sich die ausgewählte Projekt-ID, da Sie sie später benötigen.
Jetzt müssen wir diesen Code mit Ihrem Firebase-Projekt verknüpfen. Führen Sie zuerst den folgenden Befehl aus, um sich in der Firebase CLI anzumelden:
$ firebase login
Führen Sie als Nächstes den folgenden Befehl aus, um einen Projektalias zu erstellen. Ersetzen Sie $YOUR_PROJECT_ID
durch die ID Ihres Firebase-Projekts.
$ firebase use $YOUR_PROJECT_ID
Jetzt können Sie die App ausführen.
3. Emulatoren ausführen
In diesem Abschnitt führen Sie die Anwendung lokal aus. Es ist also an der Zeit, die Emulator Suite zu starten.
Emulatoren starten
Führen Sie im Quellverzeichnis des Codelab den folgenden Befehl aus, um die Emulatoren zu starten:
$ firebase emulators:start --import=./seed
Die Ausgabe sollte in etwa so aussehen:
$ 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.
Sobald die Meldung Alle Emulatoren gestartet angezeigt wird, kann die App verwendet werden.
Web-App mit den Emulatoren verbinden
In der Tabelle in den Protokollen sehen wir, dass der Cloud Firestore-Emulator Port 8080
und der Authentifizierungsemulator Port 9099
überwacht.
┌────────────────┬────────────────┬─────────────────────────────────┐ │ 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 │ └────────────────┴────────────────┴─────────────────────────────────┘
Verbinden wir nun Ihren Frontend-Code mit dem Emulator anstatt mit der Produktion. Öffnen Sie die Datei public/js/homepage.js
und suchen Sie die Funktion onDocumentReady
. Wir sehen, dass der Code auf die Standard-Firestore- und Auth-Instanzen zugreift:
public/js/homepage.js
const auth = firebaseApp.auth();
const db = firebaseApp.firestore();
Wir aktualisieren die Objekte db
und auth
, sodass sie auf die lokalen Emulatoren verweisen:
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);
}
Wenn die App nun auf Ihrem lokalen Computer ausgeführt wird, der vom Hosting-Emulator bereitgestellt wird, verweist der Firestore-Client auch auf den lokalen Emulator anstatt auf eine Produktionsdatenbank.
EmulatorUI öffnen
Rufen Sie in Ihrem Webbrowser http://127.0.0.1:4000/ auf. Nun sollten Sie die Benutzeroberfläche der Emulator Suite sehen.
Klicken Sie, um die Benutzeroberfläche des Firestore-Emulators aufzurufen. Die Sammlung items
enthält aufgrund der Daten, die mit dem Flag --import
importiert wurden, bereits Daten.
4. Anwendung ausführen
App öffnen
Rufen Sie in Ihrem Webbrowser http://127.0.0.1:5000 auf. Daraufhin sollte The Fire Store lokal auf Ihrem Computer ausgeführt werden.
App verwenden
Wählen Sie auf der Startseite einen Artikel aus und klicken Sie auf In den Einkaufswagen. Leider wird der folgende Fehler angezeigt:
Lass uns diesen Fehler beheben. Da alles in den Emulatoren ausgeführt wird, können wir experimentieren, ohne uns Gedanken über die Auswirkungen auf echte Daten machen zu müssen.
5. App debuggen
Fehler finden
Ok, sehen wir uns die Chrome-Entwicklerkonsole an. Drücken Sie Control+Shift+J
(Windows, Linux, Chrome OS) oder Command+Option+J
(Mac), um den Fehler in der Konsole anzuzeigen:
Offenbar ist ein Fehler in der addToCart
-Methode aufgetreten. Sehen wir uns das an. Wo versuchen wir in dieser Methode auf etwas namens uid
zuzugreifen und warum wäre das null
? Derzeit sieht die Methode in public/js/homepage.js
so aus:
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);
}
Aha! Wir sind nicht in der App angemeldet. Laut Firebase Authentication-Dokumenten ist auth.currentUser
null
, wenn wir nicht angemeldet sind. Dazu fügen wir eine Prüfung hinzu:
public/js/homepage.js
addToCart(id, itemData) {
// ADD THESE LINES
if (this.auth.currentUser === null) {
this.showError("You must be signed in!");
return;
}
// ...
}
App testen
Aktualisieren Sie jetzt die Seite und klicken Sie dann auf In den Einkaufswagen. Dieses Mal sollten Sie einen schöneren Fehler erhalten:
Wenn Sie jedoch in der Symbolleiste oben auf Anmelden und dann noch einmal auf In den Einkaufswagen klicken, wird der Einkaufswagen aktualisiert.
Allerdings sind die Zahlen überhaupt nicht richtig:
Keine Sorge, wir werden diesen Fehler bald beheben. Sehen wir uns zuerst einmal genauer an, was passiert ist, als du einen Artikel in deinen Einkaufswagen gelegt hast.
6. Trigger für lokale Funktionen
Wenn Sie auf In den Einkaufswagen legen klicken, wird eine Ereigniskette ausgelöst, die mehrere Emulatoren umfasst. In den Firebase CLI-Protokollen sollten Sie nach dem Hinzufügen eines Artikels in den Einkaufswagen ungefähr die folgenden Meldungen sehen:
i functions: Beginning execution of "calculateCart" i functions: Finished "calculateCart" in ~1s
Beim Erstellen dieser Logs und des beobachteten UI-Updates sind vier Schlüsselereignisse aufgetreten:
1) Firestore Write – Client
Der Firestore-Sammlung /carts/{cartId}/items/{itemId}/
wird ein neues Dokument hinzugefügt. Sie finden diesen Code in der Funktion addToCart
in 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);
}
2) Von Cloud-Funktion ausgelöst
Die Cloud Functions-Funktion calculateCart
überwacht mit dem onWrite
-Trigger, den Sie in functions/index.js
sehen können, auf alle Schreibereignisse (erstellen, aktualisieren oder löschen), die für Warenkorbartikel eintreten:
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 Write – Admin
Die Funktion calculateCart
liest alle Artikel im Einkaufswagen, addiert die Gesamtmenge und den Gesamtpreis und aktualisiert dann das Dokument „cart“ mit den neuen Gesamtwerten (siehe cartRef.update(...)
oben).
4) Firestore-Lesevorgang – Client
Das Web-Frontend ist abonniert, um Aktualisierungen zu Änderungen am Warenkorb zu erhalten. Die Karte wird in Echtzeit aktualisiert, nachdem die Cloud-Funktion die neuen Summen geschrieben und die Benutzeroberfläche aktualisiert hat, wie in public/js/homepage.js
zu sehen ist:
public/js/homepage.js
this.cartUnsub = cartRef.onSnapshot(cart => {
// The cart document was changed, update the UI
// ...
});
Zusammenfassung
Gut gemacht! Sie haben gerade eine vollständig lokale App eingerichtet, die drei verschiedene Firebase-Emulatoren für vollständig lokale Tests verwendet.
Halt – das war noch nicht alles! Im nächsten Abschnitt erfahren Sie Folgendes:
- Anleitung zum Schreiben von Einheitentests, die Firebase-Emulatoren verwenden.
- Informationen zum Entwickeln von Sicherheitsregeln mit den Firebase-Emulatoren
7. Maßgeschneiderte Sicherheitsregeln für Ihre Anwendung erstellen
Unsere Webanwendung liest und schreibt Daten, aber bisher haben wir uns noch gar keine Sorgen über die Sicherheit gemacht. Cloud Firestore verwendet ein System namens „Sicherheitsregeln“, um zu deklarieren, wer Lese- und Schreibzugriff auf Daten hat. Die Emulator Suite eignet sich hervorragend, um Prototypen für diese Regeln zu erstellen.
Öffnen Sie die Datei emulators-codelab/codelab-initial-state/firestore.rules
im Editor. Sie sehen, dass unsere Regeln in drei Hauptabschnitte unterteilt sind:
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;
}
}
}
Derzeit kann jeder Daten in unserer Datenbank lesen und schreiben. Wir möchten dafür sorgen, dass nur gültige Vorgänge durchgeführt werden und keine vertraulichen Daten offengelegt werden.
In diesem Codelab sperren wir gemäß dem Prinzip der geringsten Berechtigung alle Dokumente und fügen nach und nach Zugriff hinzu, bis alle Nutzer den benötigten Zugriff haben, aber nicht mehr. Aktualisieren wir die ersten beiden Regeln, um den Zugriff zu verweigern. Legen Sie dazu die Bedingung auf false
fest:
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. Emulatoren und Tests ausführen
Emulatoren starten
Achten Sie darauf, dass Sie sich in der Befehlszeile in emulators-codelab/codelab-initial-state/
befinden. Die Emulatoren der vorherigen Schritte werden möglicherweise noch ausgeführt. Falls nicht, starten Sie die Emulatoren noch einmal:
$ firebase emulators:start --import=./seed
Sobald die Emulatoren ausgeführt werden, können Sie lokal Tests mit ihnen ausführen.
Tests ausführen
In der Befehlszeile in einem neuen Terminaltab im Verzeichnis emulators-codelab/codelab-initial-state/
Gehen Sie zuerst zum Funktionsverzeichnis (wir bleiben für den Rest des Codelabs hier):
$ cd functions
Führen Sie nun die Mocha-Tests im Funktionsverzeichnis aus und scrollen Sie in der Ausgabe nach oben:
# 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
Derzeit haben wir vier Fehler. Beim Erstellen der Regeldatei können Sie den Fortschritt messen, indem Sie mehr bestandene Tests beobachten.
9. Sicherer Zugriff auf Einkaufswagen
Die ersten beiden Fehler sind die „Einkaufswagen“-Tests, bei denen Folgendes getestet wird:
- Nutzer können nur ihre eigenen Einkaufswagen erstellen und aktualisieren
- Nutzer können nur ihre eigenen Einkaufswagen lesen
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());
});
Lassen Sie uns diese Tests bestehen. Öffnen Sie im Editor die Datei mit den Sicherheitsregeln firestore.rules
und aktualisieren Sie die Anweisungen in 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;
}
// ...
}
}
Diese Regeln erlauben jetzt nur dem Einkaufswageninhaber Lese- und Schreibzugriff.
Um eingehende Daten und die Authentifizierung des Nutzers zu überprüfen, verwenden wir zwei Objekte, die im Kontext jeder Regel verfügbar sind:
- Das
request
-Objekt enthält Daten und Metadaten zum versuchten Vorgang. - Wenn ein Firebase-Projekt Firebase Authentication verwendet, beschreibt das
request.auth
-Objekt den Nutzer, der die Anfrage stellt.
10. Zugriff auf Einkaufswagen testen
Die Emulator Suite aktualisiert die Regeln automatisch, wenn firestore.rules
gespeichert wird. Sie können prüfen, ob der Emulator die Regeln aktualisiert hat, indem Sie auf dem Tab, auf dem der Emulator ausgeführt wird, die Meldung Rules updated
sehen:
Führen Sie die Tests noch einmal aus und prüfen Sie, ob die ersten beiden Tests jetzt bestanden werden:
$ 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
Klasse! Du hast jetzt sicheren Zugriff auf deine Einkaufswagen. Fahren wir mit dem nächsten fehlgeschlagenen Test fort.
11. Ablauf „In den Einkaufswagen“ in der Benutzeroberfläche prüfen
Derzeit können Einkaufswagen-Inhaber zwar Daten in ihren Einkaufswagen schreiben und lesen, aber keine einzelnen Artikel in ihrem Einkaufswagen. Die Inhaber haben zwar Zugriff auf das Einkaufswagendokument, haben aber keinen Zugriff auf die untergeordnete Artikelsammlung des Einkaufswagens.
Für Nutzer ist das ein Fehler.
Kehren Sie zur Web-UI zurück, die auf http://127.0.0.1:5000,
ausgeführt wird, und versuchen Sie, etwas in Ihren Einkaufswagen zu legen. In der Debugging-Konsole wird der Fehler Permission Denied
angezeigt, da wir Nutzern noch keinen Zugriff auf erstellte Dokumente in der Untersammlung items
gewährt haben.
12. Zugriff auf Artikel im Einkaufswagen zulassen
Diese beiden Tests bestätigen, dass Nutzer nur Artikel in den Einkaufswagen legen oder Artikel aus ihrem eigenen Einkaufswagen lesen können:
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
}));
});
Wir können also eine Regel schreiben, die den Zugriff zulässt, wenn der aktuelle Nutzer dieselbe UID wie die ownerUID im Warenkorbdokument hat. Da Sie keine anderen Regeln für create, update, delete
festlegen müssen, können Sie eine write
-Regel verwenden, die auf alle Anfragen zur Änderung von Daten angewendet wird.
Aktualisieren Sie die Regel für die Dokumente in der untergeordneten Elementsammlung. Mit der get
in der Bedingung wird ein Wert aus Firestore gelesen, in diesem Fall die ownerUID
im Warenkorbdokument.
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. Zugriff auf Warenkorbartikel testen
Jetzt können wir den Test noch einmal ausführen. Scrollen Sie zum Anfang der Ausgabe und prüfen Sie, ob weitere Tests bestanden wurden:
$ 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
Hört sich gut an. Jetzt sind alle unsere Tests erfolgreich. Es gibt noch einen ausstehenden Test, aber dazu kommen wir gleich.
14. Ablauf „In den Einkaufswagen“ noch einmal prüfen
Kehren Sie zum Web-Frontend (http://127.0.0.1:5000) zurück und legen Sie einen Artikel in den Einkaufswagen. Dies ist ein wichtiger Schritt, um zu überprüfen, ob unsere Tests und Regeln den vom Kunden geforderten Funktionen entsprechen. (Denken Sie daran, dass Nutzer beim letzten Testen der Benutzeroberfläche keine Artikel in den Einkaufswagen legen konnten.)
Der Client lädt die Regeln automatisch neu, wenn die firestore.rules
gespeichert wird. Du kannst also versuchen, etwas in den Warenkorb zu legen.
Zusammenfassung
Gut gemacht! Sie haben gerade die Sicherheit Ihrer App verbessert. Dies ist ein wichtiger Schritt, um sie für die Produktion vorzubereiten. Wäre es eine Produktionsanwendung, könnten wir diese Tests unserer CI-Pipeline hinzufügen. So können wir sicher sein, dass unsere Warenkorbdaten auch dann diese Zugriffssteuerungen haben, wenn andere die Regeln ändern.
Das war noch nicht alles!
Wenn Sie fortfahren, erfahren Sie Folgendes:
- Durch ein Firestore-Ereignis ausgelöste Funktion schreiben
- Tests erstellen, die über mehrere Emulatoren hinweg funktionieren
15. Cloud Functions-Tests einrichten
Bisher haben wir uns auf das Frontend unserer Webanwendung und die Firestore-Sicherheitsregeln konzentriert. Diese App verwendet jedoch auch Cloud Functions, um den Warenkorb des Nutzers auf dem neuesten Stand zu halten. Deshalb möchten wir diesen Code ebenfalls testen.
Mit der Emulator Suite können Sie Cloud Functions ganz einfach testen, auch Funktionen, die Cloud Firestore und andere Dienste verwenden.
Öffnen Sie im Editor die Datei emulators-codelab/codelab-initial-state/functions/test.js
und scrollen Sie zum letzten Test in der Datei. Derzeit ist sie als ausstehend markiert:
// 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 () => {
...
});
});
Entfernen Sie .skip
, um den Test zu aktivieren.
describe("adding an item to the cart recalculates the cart total. ", () => {
// ...
it("should sum the cost of their items", async () => {
...
});
});
Suchen Sie als Nächstes oben in der Datei nach der Variablen REAL_FIREBASE_PROJECT_ID
und ändern Sie sie in Ihre Firebase-Projekt-ID.
// CHANGE THIS LINE
const REAL_FIREBASE_PROJECT_ID = "changeme";
Wenn Sie Ihre Projekt-ID vergessen haben, finden Sie sie in der Firebase Console in den Projekteinstellungen:
16. Funktionstests durchgehen
Da bei diesem Test die Interaktion zwischen Cloud Firestore und Cloud Functions überprüft wird, ist mehr Einrichtung erforderlich als bei den Tests in den vorherigen Codelabs. Sehen wir uns diesen Test an und machen uns ein Bild davon, was erwartet wird.
Einkaufswagen erstellen
Cloud Functions wird in einer vertrauenswürdigen Serverumgebung ausgeführt und kann die vom Admin SDK verwendete Dienstkontoauthentifizierung verwenden . Zuerst initialisieren Sie eine App mit initializeAdminApp
anstelle von initializeApp
. Anschließend erstellen Sie eine DocumentReference für den Einkaufswagen, dem die Artikel hinzugefügt werden, und initialisieren den Einkaufswagen:
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 });
...
});
Funktion auslösen
Fügen Sie dann Dokumente zur Untersammlung items
des Einkaufswagendokuments hinzu, um die Funktion auszulösen. Fügen Sie zwei Elemente hinzu, damit Sie die Addition testen können, die in der Funktion ausgeführt wird.
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 });
...
});
});
Erwartungen an den Test formulieren
Mit onSnapshot()
können Sie einen Listener für Änderungen am Warenkorbdokument registrieren. onSnapshot()
gibt eine Funktion zurück, die Sie aufrufen können, um die Registrierung des Listeners aufzuheben.
Fügen Sie für diesen Test zwei Artikel hinzu, die zusammen 9,98 € kosten. Prüfen Sie dann, ob der Einkaufswagen die erwarteten itemCount
und totalPrice
enthält. In diesem Fall hat die Funktion funktioniert.
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. Tests ausführen
Die Emulatoren der vorherigen Tests werden möglicherweise noch ausgeführt. Falls nicht, starten Sie die Emulatoren. Führen Sie über die Befehlszeile
$ firebase emulators:start --import=./seed
Öffnen Sie einen neuen Terminal-Tab, lassen Sie die Emulatoren weiter laufen und wechseln Sie in das Funktionsverzeichnis. Möglicherweise ist diese Meldung noch offen, weil Sie die Sicherheitsregeln noch nicht getestet haben.
$ cd functions
Führen Sie jetzt die Unit-Tests aus. Sie sollten insgesamt fünf Tests sehen:
$ 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
Bei der Fehlerbeschreibung handelt es sich offenbar um einen Zeitüberschreitungsfehler. Das liegt daran, dass der Test darauf wartet, dass die Funktion richtig aktualisiert wird, was aber nie passiert. Jetzt können wir die Funktion schreiben, die den Test erfüllt.
18. Funktion schreiben
Um diesen Test zu korrigieren, müssen Sie die Funktion in functions/index.js
aktualisieren. Diese Funktion ist zwar teilweise geschrieben, aber noch nicht vollständig. So sieht die Funktion derzeit aus:
// 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) {
}
});
Die Funktion setzt die Warenkorbreferenz korrekt, aber anstatt die Werte von totalPrice
und itemCount
zu berechnen, werden sie durch hartcodierte Werte ersetzt.
Die
items
Untersammlung
Initialisieren Sie die neue Konstante itemsSnap
als Untersammlung items
. Durchlaufen Sie dann alle Dokumente in der Sammlung.
// 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 und itemCount berechnen
Zuerst initialisieren wir die Werte von totalPrice
und itemCount
auf null.
Fügen Sie dann die Logik in den Iterationsblock ein. Prüfen Sie zuerst, ob der Artikel einen Preis hat. Wenn für den Artikel keine Stückzahl angegeben ist, sollte standardmäßig 1
verwendet werden. Addieren Sie dann die Menge zur laufenden Summe von itemCount
. Fügen Sie abschließend den Preis des Artikels multipliziert mit der Stückzahl zur laufenden Summe von totalPrice
hinzu:
// 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) {
}
});
Sie können auch Logging hinzufügen, um den Erfolg und Fehlerstatus zu debuggen:
// 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. Tests noch einmal ausführen
Prüfen Sie in der Befehlszeile, ob die Emulatoren noch ausgeführt werden, und führen Sie die Tests noch einmal aus. Sie müssen die Emulatoren nicht neu starten, da Änderungen an den Funktionen automatisch übernommen werden. Alle Tests sollten erfolgreich sein:
$ 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)
Klasse!
20. Testen Sie es über die Storefront-Benutzeroberfläche.
Kehren Sie für den abschließenden Test zur Webanwendung zurück (http://127.0.0.1:5000/) und legen Sie einen Artikel in den Einkaufswagen.
Prüfen Sie, ob der Einkaufswagen mit dem richtigen Gesamtwert aktualisiert wird. Das freut mich.
Zusammenfassung
Sie haben einen komplexen Testfall zwischen Cloud Functions for Firebase und Cloud Firestore durchlaufen. Sie haben eine Cloud Functions-Funktion geschrieben, damit der Test bestanden wird. Außerdem haben Sie bestätigt, dass die neue Funktion in der Benutzeroberfläche funktioniert. Sie haben das alles lokal gemacht und die Emulatoren auf Ihrem eigenen Computer ausgeführt.
Außerdem haben Sie einen Webclient erstellt, der für die lokalen Emulatoren ausgeführt wird, Sicherheitsregeln zum Schutz der Daten angepasst und die Sicherheitsregeln mit den lokalen Emulatoren getestet.