Lokale Entwicklung mit der Firebase Emulator Suite

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.

2589e2f95b74fa88.png

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.

Startbildschirm der Emulators-UI

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.

4ef88d0148405d36.png

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.

939f87946bac2ee4.png

App verwenden

Wählen Sie auf der Startseite einen Artikel aus und klicken Sie auf In den Einkaufswagen. Leider wird der folgende Fehler angezeigt:

a11bd59933a8e885.png

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:

74c45df55291dab1.png

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:

c65f6c05588133f7.png

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:

239f26f02f959eef.png

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:

68c9323f2ad10f7a.png

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.

db82eef1706c9058.gif

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:

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:

5680da418b420226.png

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.)

69ad26cee520bf24.png

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.

ba5440b193e75967.gif

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:

d6d0429b700d2b21.png

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.

69ad26cee520bf24.png

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.

c6a7aeb91fe97a64.gif