1. Bevor Sie beginnen
Serverlose Backend-Tools wie Cloud Firestore und Cloud Functions sind sehr einfach zu verwenden, können jedoch schwierig zu testen sein. Mit der Firebase Local Emulator Suite können Sie lokale Versionen dieser Dienste auf Ihrem Entwicklungscomputer ausführen, sodass Sie Ihre App schnell und sicher entwickeln können.
Voraussetzungen
- Ein einfacher Editor wie Visual Studio Code, Atom oder Sublime Text
- Node.js 10.0.0 oder höher (um Node.js zu installieren, verwenden Sie nvm , um Ihre Version zu überprüfen, führen Sie
node --version
aus) - Java 7 oder höher (um Java zu installieren, verwenden Sie diese Anweisungen , um Ihre Version zu überprüfen, führen Sie
java -version
aus)
Was du tun wirst
In diesem Codelab führen Sie eine einfache Online-Shopping-App aus und debuggen sie, die auf mehreren Firebase-Diensten basiert:
- Cloud Firestore: eine global skalierbare, serverlose NoSQL-Datenbank mit Echtzeitfähigkeiten.
- Cloud Functions : ein serverloser Backend-Code, der als Reaktion auf Ereignisse oder HTTP-Anfragen ausgeführt wird.
- Firebase-Authentifizierung : ein verwalteter Authentifizierungsdienst, der in andere Firebase-Produkte integriert werden kann.
- Firebase Hosting : schnelles und sicheres Hosting für Web-Apps.
Sie verbinden die App mit der Emulator Suite, um die lokale Entwicklung zu ermöglichen.
Sie erfahren außerdem, wie Sie:
- So verbinden Sie Ihre App mit der Emulator Suite und wie die verschiedenen Emulatoren verbunden sind.
- Wie Firebase-Sicherheitsregeln funktionieren und wie man Firestore-Sicherheitsregeln mit einem lokalen Emulator testet.
- So schreiben Sie eine Firebase-Funktion, die durch Firestore-Ereignisse ausgelöst wird, und wie schreiben Sie Integrationstests, die für die Emulator Suite ausgeführt werden.
2. Einrichten
Holen Sie sich den Quellcode
In diesem Codelab beginnen Sie mit einer fast vollständigen Version des Fire Store-Beispiels. 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, wo Sie für den Rest dieses Codelabs arbeiten:
$ 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 oder zwei Minuten dauern:
# Move into the functions directory
$ cd functions
# Install dependencies
$ npm install
# Move back into the previous directory
$ cd ../
Holen Sie sich die Firebase-CLI
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
Bestätigen Sie als Nächstes, dass Sie über die neueste Version der CLI verfügen. Dieses Codelab sollte mit Version 9.0.0 oder höher funktionieren, spätere Versionen enthalten jedoch weitere Fehlerkorrekturen.
$ firebase --version 9.6.0
Stellen Sie eine Verbindung zu Ihrem Firebase-Projekt her
Wenn Sie kein Firebase-Projekt haben, erstellen Sie in der Firebase-Konsole ein neues Firebase-Projekt. Notieren Sie sich die von Ihnen gewählte Projekt-ID, Sie werden sie später benötigen.
Jetzt müssen wir diesen Code mit Ihrem Firebase-Projekt verbinden. Führen Sie zunächst den folgenden Befehl aus, um sich bei 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. Führen Sie die Emulatoren aus
In diesem Abschnitt führen Sie die App lokal aus. Dies bedeutet, dass es Zeit ist, die Emulator Suite zu starten.
Starten Sie die Emulatoren
Führen Sie im Codelab-Quellverzeichnis den folgenden Befehl aus, um die Emulatoren zu starten:
$ firebase emulators:start --import=./seed
Sie sollten eine Ausgabe wie diese sehen:
$ 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 Sie die Meldung „Alle Emulatoren gestartet“ sehen, ist die App einsatzbereit.
Verbinden Sie die Web-App mit den Emulatoren
Anhand der Tabelle in den Protokollen können wir erkennen, 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 Ihren Frontend-Code mit dem Emulator und nicht mit der Produktion. Öffnen Sie die Datei public/js/homepage.js
und suchen Sie die Funktion onDocumentReady
. Wir können sehen, dass der Code auf die Standard-Firestore- und Auth-Instanzen zugreift:
public/js/homepage.js
const auth = firebaseApp.auth();
const db = firebaseApp.firestore();
Aktualisieren wir die db
und auth
so, dass 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 und nicht auf eine Produktionsdatenbank.
Öffnen Sie die EmulatorUI
Navigieren Sie in Ihrem Webbrowser zu http://127.0.0.1:4000/ . Sie sollten die Benutzeroberfläche der Emulator Suite sehen.
Klicken Sie hier, um die Benutzeroberfläche für den Firestore-Emulator anzuzeigen. Die items
enthält aufgrund der mit dem Flag --import
importierten Daten bereits Daten.
4. Führen Sie die App aus
Öffnen Sie die App
Navigieren Sie in Ihrem Webbrowser zu http://127.0.0.1:5000 und Sie sollten sehen, dass The Fire Store lokal auf Ihrem Computer ausgeführt wird!
Nutzen Sie die App
Wählen Sie auf der Startseite einen Artikel aus und klicken Sie auf „In den Warenkorb“ . Leider wird der folgende Fehler auftreten:
Beheben wir diesen Fehler! Da alles in den Emulatoren läuft, können wir experimentieren und müssen uns keine Sorgen über die Beeinträchtigung realer Daten machen.
5. Debuggen Sie die App
Finden Sie den Fehler
Ok, schauen wir uns die Chrome-Entwicklerkonsole an. Drücken Sie Control Shift J
(Windows, Linux, Chrome OS) oder Command Option J
(Mac), um den Fehler auf der Konsole anzuzeigen:
Es scheint, dass in der Methode addToCart
ein Fehler aufgetreten ist. Sehen wir uns das einmal an. Wo versuchen wir, in dieser Methode auf etwas namens uid
zuzugreifen, und warum sollte es null
sein? Im Moment 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 der Dokumentation zur Firebase-Authentifizierung ist auth.currentUser
null
, wenn wir nicht angemeldet sind. Fügen wir dafür einen Scheck hinzu:
public/js/homepage.js
addToCart(id, itemData) {
// ADD THESE LINES
if (this.auth.currentUser === null) {
this.showError("You must be signed in!");
return;
}
// ...
}
Testen Sie die App
Aktualisieren Sie nun die Seite und klicken Sie dann auf „In den Warenkorb“ . Diesmal sollten Sie einen schöneren Fehler erhalten:
Wenn Sie jedoch in der oberen Symbolleiste auf „Anmelden“ klicken und dann erneut auf „Zum Warenkorb hinzufügen“ klicken, werden Sie sehen, dass der Warenkorb aktualisiert wurde.
Es sieht jedoch nicht so aus, als ob die Zahlen überhaupt korrekt wären:
Keine Sorge, wir werden diesen Fehler bald beheben. Lassen Sie uns zunächst genauer untersuchen, was tatsächlich passiert ist, als Sie einen Artikel in Ihren Warenkorb gelegt haben.
6. Lokale Funktionsauslöser
Wenn Sie auf „Zum Warenkorb hinzufügen“ klicken, wird eine Kette von Ereignissen ausgelöst, an denen mehrere Emulatoren beteiligt sind. In den Firebase-CLI-Protokollen sollten etwa die folgenden Meldungen angezeigt werden, nachdem Sie einen Artikel zu Ihrem Warenkorb hinzugefügt haben:
i functions: Beginning execution of "calculateCart" i functions: Finished "calculateCart" in ~1s
Zur Erstellung dieser Protokolle und der von Ihnen beobachteten Aktualisierung der Benutzeroberfläche sind vier wichtige Ereignisse eingetreten:
1) Firestore Write – 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-Funktion ausgelöst
Die Cloud-Funktion calculateCart
lauscht mithilfe des onWrite
Triggers auf alle Schreibereignisse (Erstellen, Aktualisieren oder Löschen), die bei Warenkorbartikeln auftreten, die Sie in functions/index.js
sehen können:
Funktionen/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 und addiert die Gesamtmenge und den Preis. Anschließend aktualisiert sie das Dokument „Warenkorb“ mit den neuen Gesamtbeträgen (siehe cartRef.update(...)
“ oben).
4) Firestore Read – Client
Das Web-Frontend ist abonniert, um Updates über Änderungen am Warenkorb zu erhalten. Es erhält ein Echtzeit-Update, nachdem die Cloud-Funktion die neuen Gesamtzahlen schreibt und die Benutzeroberfläche aktualisiert, wie Sie in public/js/homepage.js
sehen können:
public/js/homepage.js
this.cartUnsub = cartRef.onSnapshot(cart => {
// The cart document was changed, update the UI
// ...
});
Rekapitulieren
Gute Arbeit! Sie richten einfach eine vollständig lokale App ein, die drei verschiedene Firebase-Emulatoren für vollständig lokale Tests verwendet.
Aber warten Sie, es gibt noch mehr! Im nächsten Abschnitt erfahren Sie:
- So schreiben Sie Unit-Tests, die die Firebase-Emulatoren verwenden.
- So verwenden Sie die Firebase-Emulatoren zum Debuggen Ihrer Sicherheitsregeln.
7. Erstellen Sie Sicherheitsregeln, die auf Ihre App zugeschnitten sind
Unsere Web-App liest und schreibt Daten, aber um die Sicherheit haben wir uns bisher überhaupt keine Sorgen gemacht. Cloud Firestore verwendet ein System namens „Sicherheitsregeln“, um zu deklarieren, wer Zugriff auf Lese- und Schreibdaten hat. Die Emulator Suite ist eine großartige Möglichkeit, diese Regeln zu prototypisieren.
Öffnen Sie im Editor die Datei emulators-codelab/codelab-initial-state/firestore.rules
. Sie werden sehen, dass wir in unseren Regeln drei Hauptabschnitte haben:
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 sicherstellen, dass nur gültige Vorgänge durchkommen und keine vertraulichen Informationen preisgegeben werden.
Während dieses Codelabs werden wir gemäß dem Prinzip der geringsten Rechte alle Dokumente sperren und nach und nach Zugriff hinzufügen, bis alle Benutzer den gesamten Zugriff haben, den sie benötigen, aber nicht mehr. Aktualisieren wir die ersten beiden Regeln, um den Zugriff zu verweigern, indem wir die Bedingung auf false
setzen:
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. Führen Sie die Emulatoren und Tests aus
Starten Sie die Emulatoren
Stellen Sie in der Befehlszeile sicher, dass Sie sich in emulators-codelab/codelab-initial-state/
befinden. Möglicherweise laufen die Emulatoren der vorherigen Schritte noch. Wenn nicht, starten Sie die Emulatoren erneut:
$ firebase emulators:start --import=./seed
Sobald die Emulatoren ausgeführt werden, können Sie lokal Tests mit ihnen ausführen.
Führen Sie die Tests durch
Auf der Befehlszeile in einem neuen Terminal-Tab aus dem Verzeichnis emulators-codelab/codelab-initial-state/
Gehen Sie zunächst 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
Im Moment haben wir vier Ausfälle. Während Sie die Regeldatei erstellen, können Sie den Fortschritt messen, indem Sie beobachten, wie weitere Tests durchgeführt werden.
9. Sicherer Warenkorbzugriff
Die ersten beiden Fehler sind die „Warenkorb“-Tests, die Folgendes testen:
- Benutzer können nur ihre eigenen Warenkörbe erstellen und aktualisieren
- Benutzer können nur ihre eigenen Warenkörbe lesen
Funktionen/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 Sicherheitsregeldatei 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 Warenkorbbesitzer.
Um eingehende Daten und die Authentifizierung des Benutzers zu überprüfen, verwenden wir zwei Objekte, die im Kontext jeder Regel verfügbar sind:
- Das
request
enthält Daten und Metadaten über den versuchten Vorgang. - Wenn ein Firebase-Projekt Firebase Authentication verwendet, beschreibt das
request.auth
Objekt den Benutzer, der die Anfrage stellt.
10. Testen Sie den Warenkorbzugriff
Die Emulator Suite aktualisiert die Regeln automatisch, wenn firestore.rules
gespeichert wird. Sie können bestätigen, dass der Emulator die Regeln aktualisiert hat, indem Sie auf der Registerkarte, auf der der Emulator ausgeführt wird, nach der Meldung Rules updated
suchen:
Führen Sie die Tests erneut aus und prüfen Sie, ob die ersten beiden Tests nun 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
Gute Arbeit! Sie haben sich nun den Zugang zu den Warenkörben gesichert. Fahren wir mit dem nächsten fehlgeschlagenen Test fort.
11. Überprüfen Sie den Ablauf „Zum Warenkorb hinzufügen“ in der Benutzeroberfläche
Derzeit können Warenkorbbesitzer zwar ihren Warenkorb lesen und schreiben, einzelne Artikel in ihrem Warenkorb jedoch nicht lesen oder schreiben. Das liegt daran, dass Eigentümer zwar Zugriff auf das Warenkorbdokument, aber keinen Zugriff auf die Artikeluntersammlung des Warenkorbs haben.
Dies ist ein fehlerhafter Zustand für Benutzer.
Kehren Sie zur Web-Benutzeroberfläche zurück, die unter http://127.0.0.1:5000,
und versuchen Sie, etwas zu Ihrem Warenkorb hinzuzufügen. Sie erhalten die Fehlermeldung Permission Denied
, die in der Debug-Konsole sichtbar ist, da wir Benutzern noch keinen Zugriff auf erstellte Dokumente in der Untersammlung „ items
gewährt haben.
12. Erlauben Sie den Zugriff auf Warenkorbartikel
Diese beiden Tests bestätigen, dass Benutzer nur Artikel zu ihrem eigenen Warenkorb hinzufügen oder Artikel 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 ermöglicht, wenn der aktuelle Benutzer dieselbe UID hat wie die Eigentümer-UID im Warenkorbdokument. Da keine unterschiedlichen Regeln für create, update, delete
angegeben werden müssen, können Sie eine write
verwenden, die für alle Anforderungen gilt, die Daten ändern.
Aktualisieren Sie die Regel für die Dokumente in der Artikeluntersammlung. Der get
in der Bedingung liest einen Wert aus Firestore – 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. Testen Sie den Zugriff auf Warenkorbartikel
Jetzt können wir den Test erneut durchfü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übsch! Jetzt bestehen alle unsere Tests. Ein Test steht noch aus, aber dazu kommen wir in ein paar Schritten.
14. Überprüfen Sie den Ablauf „In den Warenkorb“ noch einmal
Kehren Sie zum Web-Frontend ( http://127.0.0.1:5000 ) zurück und legen Sie einen Artikel in den Warenkorb. Dies ist ein wichtiger Schritt, um zu bestätigen, dass unsere Tests und Regeln der vom Kunden geforderten Funktionalität entsprechen. (Denken Sie daran, dass Benutzer beim letzten Ausprobieren der Benutzeroberfläche keine Artikel in ihren Warenkorb legen konnten!)
Der Client lädt die Regeln automatisch neu, wenn die firestore.rules
gespeichert wird. Versuchen Sie also, etwas in den Warenkorb zu legen.
Rekapitulieren
Gute Arbeit! Sie haben gerade die Sicherheit Ihrer App verbessert, ein wesentlicher Schritt, um sie für die Produktion vorzubereiten! Wenn es sich um eine Produktionsanwendung handeln würde, könnten wir diese Tests zu unserer kontinuierlichen Integrationspipeline hinzufügen. Dies würde uns in Zukunft die Gewissheit geben, dass unsere Warenkorbdaten über diese Zugriffskontrollen verfügen, selbst wenn andere die Regeln ändern.
Aber warten Sie, es gibt noch mehr!
Wenn Sie weitermachen, erfahren Sie:
- So schreiben Sie eine Funktion, die durch ein Firestore-Ereignis ausgelöst wird
- So erstellen Sie Tests, die auf mehreren Emulatoren funktionieren
15. Richten Sie Cloud Functions-Tests ein
Bisher haben wir uns auf das Frontend unserer Web-App und die Firestore-Sicherheitsregeln konzentriert. Da diese App aber auch Cloud-Funktionen verwendet, um den Warenkorb des Benutzers auf dem neuesten Stand zu halten, möchten wir diesen Code ebenfalls testen.
Mit der Emulator Suite ist es ganz einfach, Cloud-Funktionen zu testen, sogar Funktionen, die Cloud Firestore und andere Dienste nutzen.
Öffnen Sie im Editor die Datei emulators-codelab/codelab-initial-state/functions/test.js
und scrollen Sie zum letzten Test in der Datei. Im Moment ist es 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 () => {
...
});
});
Um den Test zu aktivieren, entfernen Sie .skip
, sodass es wie folgt aussieht:
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 die Variable REAL_FIREBASE_PROJECT_ID
oben in der Datei und ändern Sie sie in Ihre echte Firebase-Projekt-ID:
// CHANGE THIS LINE
const REAL_FIREBASE_PROJECT_ID = "changeme";
Wenn Sie Ihre Projekt-ID vergessen haben, finden Sie Ihre Firebase-Projekt-ID in den Projekteinstellungen in der Firebase-Konsole:
16. Gehen Sie die Funktionstests durch
Da dieser Test die Interaktion zwischen Cloud Firestore und Cloud Functions validiert, erfordert er mehr Einrichtung als die Tests in den vorherigen Codelabs. Lassen Sie uns diesen Test durchgehen und eine Vorstellung davon bekommen, was er erwartet.
Erstellen Sie einen Warenkorb
Cloud Functions werden in einer vertrauenswürdigen Serverumgebung ausgeführt und können 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 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 });
...
});
Lösen Sie die Funktion aus
Fügen Sie dann Dokumente zur Untersammlung items
unseres Warenkorbdokuments hinzu, um die Funktion auszulösen. Fügen Sie zwei Elemente hinzu, um sicherzustellen, dass Sie die Hinzufügung testen, die in der Funktion erfolgt.
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 });
...
});
});
Legen Sie Testerwartungen fest
Verwenden Sie onSnapshot()
, um einen Listener für alle Änderungen am Warenkorbdokument zu 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. Überprüfen Sie dann, ob der Warenkorb die erwartete itemCount
und totalPrice
aufweist. Wenn ja, dann hat die Funktion ihre Aufgabe 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. Führen Sie die Tests durch
Möglicherweise laufen noch die Emulatoren der vorherigen Tests. Wenn nicht, starten Sie die Emulatoren. Führen Sie über die Befehlszeile Folgendes aus:
$ firebase emulators:start --import=./seed
Öffnen Sie einen neuen Terminal-Tab (lassen Sie die Emulatoren laufen) und wechseln Sie in das Funktionsverzeichnis. Möglicherweise ist dies aufgrund der Sicherheitsregeltests noch geöffnet.
$ cd functions
Führen Sie nun die Unit-Tests aus. Sie sollten insgesamt 5 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
Wenn Sie sich den konkreten Fehler ansehen, scheint es sich um einen Timeout-Fehler zu handeln. Dies liegt daran, dass der Test darauf wartet, dass die Funktion korrekt aktualisiert wird, dies jedoch nie der Fall ist. Jetzt können wir die Funktion schreiben, um den Test zu bestehen.
18. Schreiben Sie eine Funktion
Um diesen Test zu beheben, müssen Sie die Funktion in functions/index.js
aktualisieren. Obwohl ein Teil dieser Funktion geschrieben ist, ist sie 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 statt die Werte von totalPrice
und itemCount
zu berechnen, aktualisiert sie sie auf fest codierte Werte.
Rufen Sie die Datei ab und durchlaufen Sie sie
Untersammlung items
Initialisieren Sie eine neue Konstante, itemsSnap
, um die Untersammlung items
zu sein. 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) {
}
});
Berechnen Sie totalPrice und itemCount
Lassen Sie uns zunächst die Werte von totalPrice
und itemCount
auf Null initialisieren.
Fügen Sie dann die Logik zu unserem Iterationsblock hinzu. Überprüfen Sie zunächst, ob für den Artikel ein Preis angegeben ist. Wenn für den Artikel keine Menge angegeben ist, verwenden Sie den Standardwert 1
. Fügen Sie dann die Menge zur laufenden Summe von itemCount
hinzu. Addieren Sie abschließend den Preis des Artikels multipliziert mit der Menge zur laufenden Summe von 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) {
}
});
Sie können auch eine Protokollierung hinzufügen, um das Debuggen von Erfolgs- und Fehlerzuständen zu unterstützen:
// 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. Führen Sie die Tests erneut durch
Stellen Sie in der Befehlszeile sicher, dass die Emulatoren noch ausgeführt werden, und führen Sie die Tests erneut aus. Sie müssen die Emulatoren nicht neu starten, da sie Änderungen an den Funktionen automatisch übernehmen. Sie sollten sehen, dass alle Tests bestanden wurden:
$ 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)
Gute Arbeit!
20. Probieren Sie es mit der Storefront-Benutzeroberfläche aus
Kehren Sie für den letzten Test zur Web-App ( http://127.0.0.1:5000/ ) zurück und legen Sie einen Artikel in den Warenkorb.
Bestätigen Sie, dass der Warenkorb mit der korrekten Gesamtsumme aktualisiert wird. Fantastisch!
Rekapitulieren
Sie haben einen komplexen Testfall zwischen Cloud Functions for Firebase und Cloud Firestore durchlaufen. Sie haben eine Cloud-Funktion geschrieben, um den Test zu bestehen. Sie haben auch bestätigt, dass die neue Funktionalität in der Benutzeroberfläche funktioniert! Sie haben dies alles lokal durchgeführt und die Emulatoren auf Ihrem eigenen Computer ausgeführt.
Sie haben außerdem einen Web-Client erstellt, der mit den lokalen Emulatoren ausgeführt wird, Sicherheitsregeln zum Schutz der Daten angepasst und die Sicherheitsregeln mithilfe der lokalen Emulatoren getestet.