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
- Ein einfacher Editor wie Visual Studio Code, Atom oder Sublime Text
- Node.js 10.0.0 oder höher (nvm verwenden, um Node.js zu installieren;
node --version
ausführen, um die Version zu prüfen) - Java 7 oder höher (Installationsanleitung; Version prüfen:
java -version
)
Aufgabe
In diesem Codelab führen Sie eine einfache Onlineshopping-App aus und debuggen sie. Die App wird von mehreren Firebase-Diensten unterstützt:
- 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 in andere Firebase-Produkte eingebunden werden kann.
- Firebase Hosting: schnelles und sicheres Hosting für Web-Apps.
Sie stellen eine Verbindung der App zur Emulator Suite her, um die lokale Entwicklung zu ermöglichen.
Außerdem erfahren Sie, wie Sie
- Hier erfahren 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 für die Emulator Suite ausgeführt werden.
2. Einrichten
Quellcode abrufen
In diesem Codelab beginnen Sie mit einer fast vollständigen Version des Beispiels „The Fire Store“. Als Erstes müssen Sie also den Quellcode klonen:
$ git clone https://github.com/firebase/emulators-codelab.git
Wechseln Sie dann in das Codelab-Verzeichnis, in dem Sie für den Rest dieses Codelabs arbeiten werden:
$ cd emulators-codelab/codelab-initial-state
Installieren Sie nun die Abhängigkeiten, damit Sie den Code ausführen können. Wenn Sie eine langsamere Internetverbindung haben, 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 herunterladen
Die Emulator Suite ist Teil der Firebase CLI (Befehlszeile), 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 aktuelle Version der CLI haben. Dieses Codelab sollte mit Version 9.0.0 oder höher funktionieren. Spätere Versionen enthalten jedoch mehr Fehlerkorrekturen.
$ firebase --version 9.6.0
Mit Ihrem Firebase-Projekt verbinden
Firebase-Projekt erstellen
- Melden Sie sich mit Ihrem Google-Konto in der Firebase Console an.
- Klicken Sie auf die Schaltfläche, um ein neues Projekt zu erstellen, und geben Sie dann einen Projektnamen ein (z. B.
Emulators Codelab
).
- Klicken Sie auf Weiter.
- Lesen und akzeptieren Sie bei Aufforderung die Firebase-Nutzungsbedingungen und klicken Sie dann auf Weiter.
- (Optional) Aktivieren Sie die KI-Unterstützung in der Firebase Console (als „Gemini in Firebase“ bezeichnet).
- Für dieses Codelab benötigen Sie kein Google Analytics. Deaktivieren Sie daher die Google Analytics-Option.
- Klicken Sie auf Projekt erstellen, warten Sie, bis Ihr Projekt bereitgestellt wurde, und klicken Sie dann auf Weiter.
Code mit Ihrem Firebase-Projekt verbinden
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 App lokal aus. Das bedeutet, dass es an der Zeit ist, die Emulator Suite zu starten.
Emulatoren starten
Führen Sie im Quellverzeichnis des Codelabs 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
Anhand der Tabelle in den Logs können wir sehen, dass der Cloud Firestore-Emulator Port 8080
und der Authentifizierungs-Emulator 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 │ └────────────────┴────────────────┴─────────────────────────────────┘
Wir verbinden Ihren Frontend-Code mit dem Emulator statt mit der Produktion. Öffnen Sie die Datei public/js/homepage.js
und suchen Sie die Funktion onDocumentReady
. Der Code greift auf die Standardinstanzen von Firestore und Auth zu:
public/js/homepage.js
const auth = firebaseApp.auth();
const db = firebaseApp.firestore();
Aktualisieren wir die Objekte db
und auth
, damit 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 jetzt auf Ihrem lokalen Computer ausgeführt wird (bereitgestellt vom Hosting-Emulator), verweist der Firestore-Client auch auf den lokalen Emulator und nicht auf eine Produktionsdatenbank.
EmulatorUI öffnen
Rufen Sie in Ihrem Webbrowser http://127.0.0.1:4000/ auf. Die Emulator Suite-Benutzeroberfläche sollte angezeigt werden.
Klicken Sie hier, um die Benutzeroberfläche für den Firestore-Emulator aufzurufen. Die Sammlung items
enthält bereits Daten, die mit dem Flag --import
importiert wurden.
4. Anwendung ausführen
App öffnen
Rufen Sie in Ihrem Webbrowser http://127.0.0.1:5000 auf. The Fire Store sollte jetzt 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 tritt der folgende Fehler auf:
Lass uns diesen Fehler beheben. Da alles in den Emulatoren ausgeführt wird, können wir experimentieren, ohne uns Sorgen machen zu müssen, dass echte Daten beeinträchtigt werden.
5. App debuggen
Fehler finden
Sehen wir uns die Chrome-Entwicklerkonsole an. Drücken Sie Control+Shift+J
(Windows, Linux, ChromeOS) oder Command+Option+J
(Mac), um den Fehler in der Konsole zu sehen:
Offenbar ist in der Methode addToCart
ein Fehler aufgetreten. Sehen wir uns das einmal an. Wo versuchen wir in dieser Methode, auf etwas namens uid
zuzugreifen, und warum sollte es null
sein? 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. Gemäß der Firebase Authentication-Dokumentation ist auth.currentUser
null
, wenn wir nicht angemeldet sind. Fügen wir eine Prüfung dafür 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 die Seite und klicken Sie dann auf In den Warenkorb. Dieses Mal sollte eine aussagekräftigere Fehlermeldung angezeigt werden:
Wenn Sie jedoch in der oberen Symbolleiste auf Anmelden und dann noch einmal auf In den Einkaufswagen klicken, wird der Einkaufswagen aktualisiert.
Die Zahlen scheinen jedoch nicht zu stimmen:
Keine Sorge, wir werden diesen Fehler bald beheben. Sehen wir uns zuerst an, was genau passiert, wenn Sie einen Artikel in den Einkaufswagen legen.
6. Trigger für lokale Funktionen
Wenn Sie auf In den Einkaufswagen klicken, wird eine Kette von Ereignissen ausgelöst, an denen mehrere Emulatoren beteiligt sind. In den Firebase CLI-Logs sollten nach dem Hinzufügen eines Artikels zum Einkaufswagen Meldungen wie die folgenden angezeigt werden:
i functions: Beginning execution of "calculateCart" i functions: Finished "calculateCart" in ~1s
Es gab vier wichtige Ereignisse, die zu diesen Protokollen und der beobachteten Aktualisierung der Benutzeroberfläche geführt haben:
1) Firestore-Schreibvorgang – Client
Der Firestore-Sammlung /carts/{cartId}/items/{itemId}/
wird ein neues Dokument hinzugefügt. Sie können diesen Code in der Funktion addToCart
in public/js/homepage.js
sehen:
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-Trigger
Die Cloud-Funktion calculateCart
überwacht alle Schreibereignisse (Erstellen, Aktualisieren oder Löschen), die für Warenkorbartikel auftreten, indem sie den Trigger onWrite
verwendet, den Sie in functions/index.js
sehen:
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 Warenkorb, addiert die Gesamtmenge und den Preis und aktualisiert dann das Dokument „cart“ mit den neuen Summen (siehe cartRef.update(...)
oben).
4) Firestore-Lesevorgang – Client
Das Web-Frontend ist so konfiguriert, dass es Updates zu Änderungen am Warenkorb erhält. Sie erhält ein Echtzeit-Update, 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:
- So schreiben Sie Unittests, die die Firebase-Emulatoren verwenden.
- So verwenden Sie die Firebase-Emulatoren, um Ihre Sicherheitsregeln zu debuggen.
7. Sicherheitsregeln für Ihre App erstellen
Unsere Web-App liest und schreibt Daten, aber bisher haben wir uns noch nicht wirklich um die Sicherheit gekümmert. Cloud Firestore verwendet ein System namens „Sicherheitsregeln“, um festzulegen, wer Zugriff auf das Lesen und Schreiben von Daten hat. Die Emulator Suite ist eine gute Möglichkeit, diese Regeln zu testen.
Öffnen Sie im Editor die Datei emulators-codelab/codelab-initial-state/firestore.rules
. Die Regeln sind in drei Hauptabschnitte unterteilt:
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 unsere Datenbank lesen und schreiben. Wir möchten sicherstellen, dass nur gültige Vorgänge ausgeführt werden und keine vertraulichen Informationen nach außen gelangen.
In diesem Codelab werden wir gemäß dem Prinzip der geringsten Berechtigung alle Dokumente sperren und den Zugriff nach und nach hinzufügen, bis alle Nutzer den benötigten Zugriff haben, aber nicht mehr. Aktualisieren wir die ersten beiden Regeln, um den Zugriff zu verweigern, indem wir die Bedingung auf false
festlegen:
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 im Verzeichnis emulators-codelab/codelab-initial-state/
befinden. Möglicherweise sind die Emulatoren aus den vorherigen Schritten noch aktiv. Falls nicht, starten Sie die Emulatoren noch einmal:
$ firebase emulators:start --import=./seed
Sobald die Emulatoren ausgeführt werden, können Sie lokale Tests für sie ausführen.
Tests ausführen
Führen Sie in der Befehlszeile auf einem neuen Terminaltab im Verzeichnis emulators-codelab/codelab-initial-state/
folgenden Befehl aus:
Wechseln Sie zuerst in das 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 zum Anfang der Ausgabe:
# 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. Während Sie die Regelfile erstellen, können Sie den Fortschritt daran erkennen, dass immer mehr Tests bestanden werden.
9. Sicherer Zugriff auf den Warenkorb
Bei den ersten beiden Fehlern handelt es sich um die Tests für den Einkaufswagen, bei denen Folgendes geprüft wird:
- Nutzer können nur eigene 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());
});
Sorgen wir dafür, dass diese Tests bestanden werden. Ö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 noch Lese- und Schreibzugriff durch den Warenkorbinhaber.
Zur Überprüfung eingehender Daten und der Authentifizierung des Nutzers verwenden wir zwei Objekte, die im Kontext jeder Regel verfügbar sind:
- Das Objekt
request
enthält Daten und Metadaten zum Vorgang, der versucht wird. - Wenn in einem Firebase-Projekt Firebase Authentication verwendet wird, beschreibt das Objekt
request.auth
den Nutzer, der die Anfrage stellt.
10. Warenkorbzugriff testen
Die Emulator Suite aktualisiert die Regeln automatisch, wenn firestore.rules
gespeichert wird. Sie können prüfen, ob die Regeln im Emulator aktualisiert wurden, indem Sie auf dem Tab, auf dem der Emulator ausgeführt wird, nach der Meldung Rules updated
suchen:
Führen Sie die Tests noch einmal aus und prüfen Sie, ob die ersten beiden Tests jetzt erfolgreich sind:
$ 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! Sie haben jetzt sicheren Zugriff auf Einkaufswagen. Machen wir mit dem nächsten fehlgeschlagenen Test weiter.
11. „In den Warenkorb legen“-Ablauf in der Benutzeroberfläche prüfen
Derzeit können Warenkorbinhaber zwar ihren Warenkorb lesen und in ihn schreiben, aber nicht einzelne Artikel darin lesen oder in sie schreiben. Das liegt daran, dass Inhaber zwar Zugriff auf das Einkaufswagendokument haben, aber nicht auf die Unterkollektion „items“ des Einkaufswagens.
Das ist ein fehlerhafter Zustand für Nutzer.
Kehren Sie zur Web-UI zurück, die unter http://127.0.0.1:5000,
ausgeführt wird, und versuchen Sie, etwas in den Einkaufswagen zu legen. Sie erhalten einen Permission Denied
-Fehler, der in der Debug-Konsole angezeigt wird, da wir Nutzern noch keinen Zugriff auf erstellte Dokumente in der Unterkollektion items
gewährt haben.
12. Zugriff auf Artikel im Einkaufswagen erlauben
Diese beiden Tests bestätigen, dass Nutzer nur Artikel in ihren eigenen Einkaufswagen legen oder daraus 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 erlaubt, wenn die aktuelle Nutzer-UID mit der ownerUID im Warenkorbdokument übereinstimmt. Da keine unterschiedlichen Regeln für create, update, delete
angegeben werden müssen, können Sie eine write
-Regel verwenden, die für alle Anfragen gilt, mit denen Daten geändert werden.
Aktualisieren Sie die Regel für die Dokumente in der Untergruppe „items“. Mit dem get
in der Bedingung wird ein Wert aus Firestore gelesen, in diesem Fall ownerUID
aus dem 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 Einkaufswagenartikel testen
Jetzt können wir den Test noch einmal ausführen. Scrollen Sie zum Anfang der Ausgabe und prüfen Sie, ob mehr 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 steht noch ein Test aus, aber dazu kommen wir in ein paar Schritten.
14. Ablauf „In den Warenkorb legen“ 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. Dieser Schritt ist wichtig, um zu bestätigen, dass unsere Tests und Regeln der vom Kunden geforderten Funktionalität entsprechen. (Beim letzten Test der Benutzeroberfläche konnten Nutzer keine Artikel in den Einkaufswagen legen.)
Der Client lädt die Regeln automatisch neu, wenn firestore.rules
gespeichert wird. Versuchen Sie also, etwas in den Einkaufswagen zu legen.
Zusammenfassung
Gut gemacht! Sie haben gerade die Sicherheit Ihrer App verbessert. Das ist ein wichtiger Schritt, um sie für die Produktion vorzubereiten. Wenn es sich um eine Produktions-App handeln würde, könnten wir diese Tests unserer Continuous Integration-Pipeline hinzufügen. So können wir sicher sein, dass unsere Warenkorbdaten auch dann über diese Zugriffssteuerung verfügen, wenn andere die Regeln ändern.
Und das war noch längst nicht alles!
Wenn Sie fortfahren, erfahren Sie Folgendes:
- Durch ein Firestore-Ereignis ausgelöste Funktion schreiben
- Tests erstellen, die auf mehreren Emulatoren funktionieren
15. Cloud Functions-Tests einrichten
Bisher haben wir uns auf das Frontend unserer Web-App und die Firestore-Sicherheitsregeln konzentriert. Diese App verwendet aber auch Cloud Functions, um den Warenkorb des Nutzers auf dem neuesten Stand zu halten. Daher möchten wir auch diesen Code testen.
Mit der Emulator Suite lassen sich 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 tatsächliche 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 validiert wird, ist mehr Einrichtung erforderlich als bei den Tests in den vorherigen Codelabs. Sehen wir uns diesen Test einmal an, um eine Vorstellung davon zu bekommen, was erwartet wird.
Einkaufswagen erstellen
Cloud Functions wird in einer vertrauenswürdigen Serverumgebung ausgeführt und kann die Dienstkontoauthentifizierung verwenden, die vom Admin SDK verwendet wird . Zuerst initialisieren Sie eine App mit initializeAdminApp
anstelle von initializeApp
. Anschließend erstellen Sie eine DocumentReference für den Warenkorb, dem wir Artikel hinzufügen, und initialisieren den Warenkorb:
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 dem Unterdokument items
unseres Warenkorbdokuments Dokumente hinzu, um die Funktion auszulösen. Fügen Sie zwei Elemente hinzu, um sicherzustellen, dass die Addition in der Funktion getestet 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 festlegen
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 den Listener zu deregistrieren.
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. Wenn ja, hat die Funktion ihren Zweck erfüllt.
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
Möglicherweise werden die Emulatoren aus den vorherigen Tests noch ausgeführt. Wenn nicht, starten Sie die Emulatoren. Führen Sie in der Befehlszeile folgenden Befehl aus:
$ firebase emulators:start --import=./seed
Öffnen Sie einen neuen Terminaltab (lassen Sie die Emulatoren laufen) und wechseln Sie in das Funktionsverzeichnis. Möglicherweise ist sie noch von den Tests der Sicherheitsregeln geöffnet.
$ cd functions
Führen Sie jetzt die Einheitentests aus. Es sollten insgesamt fünf Tests angezeigt werden:
$ 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
Wenn Sie sich den spezifischen Fehler ansehen, scheint es sich um einen Zeitüberschreitungsfehler zu handeln. Das liegt daran, dass der Test darauf wartet, dass die Funktion korrekt aktualisiert wird, was aber nie geschieht. Jetzt können wir die Funktion schreiben, die den Test besteht.
18. Funktion schreiben
Um diesen Test zu bestehen, müssen Sie die Funktion in functions/index.js
aktualisieren. Ein Teil dieser Funktion ist zwar geschrieben, aber sie ist 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 legt die Warenkorbreferenz korrekt fest, aktualisiert die Werte von totalPrice
und itemCount
dann aber auf hartcodierte Werte, anstatt sie zu berechnen.
Rufe die ab und durchlaufe sie.
items
subcollection
Initialisieren Sie eine neue Konstante, itemsSnap
, als items
-Unterkollektion. Anschließend durchlaufen Sie 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
mit null.
Fügen Sie dann die Logik zu unserem Iterationsblock hinzu. Prüfen Sie zuerst, ob der Artikel einen Preis hat. Wenn für den Artikel keine Menge angegeben ist, lassen Sie den Standardwert 1
zu. Fügen Sie die Menge dann der laufenden Summe von itemCount
hinzu. Fügen Sie zum Schluss den Preis des Artikels multipliziert mit der Menge zum laufenden Gesamtbetrag 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 Erfolgs- 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 wiederholen
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. Über die Storefront-Benutzeroberfläche ausprobieren
Kehren Sie für den letzten Test zur Web-App ( http://127.0.0.1:5000/) zurück und fügen Sie dem Einkaufswagen einen Artikel hinzu.
Prüfen Sie, ob der Warenkorb mit dem richtigen Gesamtbetrag aktualisiert wird. Das freut mich.
Zusammenfassung
Sie haben einen komplexen Testlauf 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 alles lokal ausgeführt und die Emulatoren auf Ihrem eigenen Computer ausgeführt.
Sie haben außerdem einen Webclient erstellt, der mit den lokalen Emulatoren ausgeführt wird, Sicherheitsregeln zum Schutz der Daten angepasst und die Sicherheitsregeln mit den lokalen Emulatoren getestet.