Sviluppo locale con Firebase Emulator Suite

1. Prima di iniziare

Gli strumenti di backend serverless come Cloud Firestore e Cloud Functions sono molto facili da usare, ma possono essere difficili da testare. Firebase Local Emulator Suite ti consente di eseguire versioni locali di questi servizi sulla tua macchina di sviluppo, in modo da poter sviluppare la tua app in modo rapido e sicuro.

Prerequisiti

  • Un editor semplice come Visual Studio Code, Atom o Sublime Text
  • Node.js 10.0.0 o versioni successive (per installare Node.js, utilizza nvm; per controllare la versione, esegui node --version)
  • Java 7 o versioni successive (per installare Java, segui queste istruzioni; per controllare la versione, esegui java -version)

Attività previste

In questo codelab, eseguirai e debuggerai una semplice app di shopping online basata su più servizi Firebase:

  • Cloud Firestore:un database NoSQL serverless e scalabile a livello globale con funzionalità in tempo reale.
  • Cloud Functions: un codice di backend serverless che viene eseguito in risposta a eventi o richieste HTTP.
  • Firebase Authentication: un servizio di autenticazione gestito che si integra con altri prodotti Firebase.
  • Firebase Hosting: hosting rapido e sicuro per le app web.

Collegherai l'app a Emulator Suite per abilitare lo sviluppo locale.

2589e2f95b74fa88.png

Imparerai anche a:

  • Come connettere l'app a Emulator Suite e come sono connessi i vari emulatori.
  • Come funzionano le regole di sicurezza Firebase e come testare le regole di sicurezza di Firestore rispetto a un emulatore locale.
  • Come scrivere una funzione Firebase attivata da eventi Firestore e come scrivere test di integrazione eseguiti su Emulator Suite.

2. Configura

Recuperare il codice sorgente

In questo codelab, inizierai con una versione quasi completa dell'esempio di The Fire Store, quindi la prima cosa da fare è clonare il codice sorgente:

$ git clone https://github.com/firebase/emulators-codelab.git

Poi passa alla directory del codelab, dove lavorerai per il resto del codelab:

$ cd emulators-codelab/codelab-initial-state

Ora installa le dipendenze per poter eseguire il codice. Se la connessione a internet è lenta, l'operazione potrebbe richiedere un minuto o due:

# Move into the functions directory
$ cd functions

# Install dependencies
$ npm install

# Move back into the previous directory
$ cd ../

Scarica la CLI di Firebase

Emulator Suite fa parte dell'interfaccia a riga di comando di Firebase, che può essere installata sulla tua macchina con il seguente comando:

$ npm install -g firebase-tools

Poi, verifica di avere l'ultima versione della CLI. Questo codelab dovrebbe funzionare con la versione 9.0.0 o successive, ma le versioni successive includono più correzioni di bug.

$ firebase --version
9.6.0

Connettersi al progetto Firebase

Crea un progetto Firebase

  1. Accedi alla console Firebase utilizzando il tuo Account Google.
  2. Fai clic sul pulsante per creare un nuovo progetto, quindi inserisci un nome per il progetto (ad esempio Emulators Codelab).
  3. Fai clic su Continua.
  4. Se richiesto, leggi e accetta i termini di Firebase, quindi fai clic su Continua.
  5. (Facoltativo) Attiva l'assistenza AI nella console Firebase (denominata "Gemini in Firebase").
  6. Per questo codelab non hai bisogno di Google Analytics, quindi disattiva l'opzione Google Analytics.
  7. Fai clic su Crea progetto, attendi il provisioning del progetto, poi fai clic su Continua.

Collega il codice al progetto Firebase

Ora dobbiamo collegare questo codice al tuo progetto Firebase. Per prima cosa, esegui questo comando per accedere all'interfaccia a riga di comando di Firebase:

$ firebase login

Quindi, esegui il comando seguente per creare un alias del progetto. Sostituisci $YOUR_PROJECT_ID con l'ID del tuo progetto Firebase.

$ firebase use $YOUR_PROJECT_ID

Ora puoi eseguire l'app.

3. Esegui gli emulatori

In questa sezione eseguirai l'app in locale. Ciò significa che è il momento di avviare Emulator Suite.

Avvia gli emulatori

Dall'interno della directory di origine del codelab, esegui il seguente comando per avviare gli emulatori:

$ firebase emulators:start --import=./seed

Dovresti vedere un output simile al seguente:

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

Quando viene visualizzato il messaggio Tutti gli emulatori sono stati avviati, l'app è pronta per l'uso.

Connettere l'app web agli emulatori

In base alla tabella nei log, possiamo vedere che l'emulatore Cloud Firestore è in ascolto sulla porta 8080 e l'emulatore Authentication è in ascolto sulla porta 9099.

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting        │ 127.0.0.1:5000 │ n/a                             │
└────────────────┴────────────────┴─────────────────────────────────┘

Colleghiamo il codice frontend all'emulatore anziché alla produzione. Apri il file public/js/homepage.js e trova la funzione onDocumentReady. Possiamo vedere che il codice accede alle istanze standard di Firestore e Auth:

public/js/homepage.js

  const auth = firebaseApp.auth();
  const db = firebaseApp.firestore();

Aggiorniamo gli oggetti db e auth in modo che puntino agli emulatori locali:

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);
  }

Ora, quando l'app viene eseguita sulla macchina locale (servita dall'emulatore Hosting), il client Firestore punta anche all'emulatore locale anziché a un database di produzione.

Apri EmulatorUI

Nel browser web, vai alla pagina http://127.0.0.1:4000/. Dovresti visualizzare l'interfaccia utente di Emulator Suite.

Schermata Home dell'interfaccia utente degli emulatori

Fai clic per visualizzare la UI dell'emulatore Firestore. La raccolta items contiene già dati a causa dei dati importati con il flag --import.

4ef88d0148405d36.png

4. Esegui l'app

Apri l'app

Nel browser web, vai alla pagina http://127.0.0.1:5000 e dovresti vedere The Fire Store in esecuzione in locale sul tuo computer.

939f87946bac2ee4.png

Utilizzare l'app

Scegli un articolo nella home page e fai clic su Aggiungi al carrello. Purtroppo, si verifica il seguente errore:

a11bd59933a8e885.png

Correggiamo questo bug. Poiché tutto viene eseguito negli emulatori, possiamo sperimentare senza preoccuparci di influire sui dati reali.

5. Esegui il debug dell'app

Trovare il bug

Ok, diamo un'occhiata alla console per sviluppatori di Chrome. Premi Control+Shift+J (Windows, Linux, ChromeOS) o Command+Option+J (Mac) per visualizzare l'errore nella console:

74c45df55291dab1.png

Sembra che si sia verificato un errore nel metodo addToCart, diamo un'occhiata. Dove proviamo ad accedere a qualcosa chiamato uid in questo metodo e perché dovrebbe essere null? Al momento, il metodo ha questo aspetto 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);
  }

Aha! Non abbiamo eseguito l'accesso all'app. Secondo la documentazione di Firebase Authentication, quando non abbiamo eseguito l'accesso, auth.currentUser è null. Aggiungiamo un controllo:

public/js/homepage.js

  addToCart(id, itemData) {
    // ADD THESE LINES
    if (this.auth.currentUser === null) {
      this.showError("You must be signed in!");
      return;
    }

    // ...
  }

Testare l'app

Ora aggiorna la pagina e fai clic su Aggiungi al carrello. Questa volta dovresti ricevere un errore più chiaro:

c65f6c05588133f7.png

Tuttavia, se fai clic su Accedi nella barra degli strumenti in alto e poi di nuovo su Aggiungi al carrello, vedrai che il carrello è aggiornato.

Tuttavia, i numeri non sembrano corretti:

239f26f02f959eef.png

Non preoccuparti, risolveremo il bug a breve. Innanzitutto, analizziamo nel dettaglio cosa è successo quando hai aggiunto un articolo al carrello.

6. Trigger di funzioni locali

Se fai clic su Aggiungi al carrello, si avvia una catena di eventi che coinvolgono più emulatori. Nei log della CLI Firebase, dopo aver aggiunto un articolo al carrello, dovresti visualizzare messaggi simili ai seguenti:

i  functions: Beginning execution of "calculateCart"
i  functions: Finished "calculateCart" in ~1s

Si sono verificati quattro eventi chiave che hanno prodotto questi log e l'aggiornamento della UI che hai osservato:

68c9323f2ad10f7a.png

1) Scrittura Firestore - Client

Un nuovo documento viene aggiunto alla raccolta Firestore /carts/{cartId}/items/{itemId}/. Puoi visualizzare questo codice nella funzione addToCart all'interno di 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) Cloud Function Triggered

La funzione Cloud Functions calculateCart è in ascolto di qualsiasi evento di scrittura (creazione, aggiornamento o eliminazione) che si verifica per gli articoli del carrello utilizzando il trigger onWrite, che puoi vedere in functions/index.js:

functions/index.js

exports.calculateCart = functions.firestore
    .document("carts/{cartId}/items/{itemId}")
    .onWrite(async (change, context) => {
      try {
        let totalPrice = 125.98;
        let itemCount = 8;

        const cartRef = db.collection("carts").doc(context.params.cartId);

        await cartRef.update({
          totalPrice,
          itemCount
        });
      } catch(err) {
      }
    }
);

3) Firestore Write - Admin

La funzione calculateCart legge tutti gli articoli nel carrello e somma la quantità e il prezzo totali, quindi aggiorna il documento "carrello" con i nuovi totali (vedi cartRef.update(...) sopra).

4) Firestore Read - Client

Il frontend web è iscritto per ricevere aggiornamenti sulle modifiche al carrello. Riceve un aggiornamento in tempo reale dopo che la funzione Cloud scrive i nuovi totali e aggiorna la UI, come puoi vedere in public/js/homepage.js:

public/js/homepage.js

this.cartUnsub = cartRef.onSnapshot(cart => {
   // The cart document was changed, update the UI
   // ...
});

Riepilogo

Ottimo! Hai appena configurato un'app completamente locale che utilizza tre diversi emulatori Firebase per i test completamente locali.

db82eef1706c9058.gif

Ma non è tutto! Nella sezione successiva imparerai:

  • Come scrivere test delle unità che utilizzano gli emulatori Firebase.
  • Come utilizzare gli emulatori Firebase per eseguire il debug delle regole di sicurezza.

7. Creare regole di sicurezza personalizzate per la tua app

La nostra app web legge e scrive dati, ma finora non ci siamo preoccupati della sicurezza. Cloud Firestore utilizza un sistema chiamato "Regole di sicurezza" per dichiarare chi ha accesso alla lettura e alla scrittura dei dati. Emulator Suite è un ottimo modo per creare prototipi di queste regole.

Nell'editor, apri il file emulators-codelab/codelab-initial-state/firestore.rules. Vedrai che le nostre regole sono suddivise in tre sezioni principali:

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;
    }
  }
}

Al momento chiunque può leggere e scrivere dati nel nostro database. Vogliamo assicurarci che vengano eseguite solo operazioni valide e che non vengano divulgate informazioni sensibili.

Durante questo codelab, seguendo il principio del privilegio minimo, bloccheremo tutti i documenti e aggiungeremo gradualmente l'accesso finché tutti gli utenti non avranno tutto l'accesso di cui hanno bisogno, ma non di più. Aggiorniamo le prime due regole per negare l'accesso impostando la condizione su false:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // User's cart metadata
    match /carts/{cartID} {
      // UPDATE THIS LINE
      allow read, write: if false;
    }

    // Items inside the user's cart
    match /carts/{cartID}/items/{itemID} {
      // UPDATE THIS LINE
      allow read, write: if false;
    }

    // All items available in the store. Users can read
    // items but never write them.
    match /items/{itemID} {
      allow read: if true;
    }
  }
}

8. Esegui gli emulatori e i test

Avvia gli emulatori

Nella riga di comando, assicurati di trovarti in emulators-codelab/codelab-initial-state/. Potresti avere ancora gli emulatori in esecuzione dai passaggi precedenti. In caso contrario, riavvia gli emulatori:

$ firebase emulators:start --import=./seed

Una volta eseguiti gli emulatori, puoi eseguire test localmente.

Esegui i test

Nella riga di comando in una nuova scheda del terminale dalla directory emulators-codelab/codelab-initial-state/

Innanzitutto, spostati nella directory delle funzioni (rimarremo qui per il resto del codelab):

$ cd functions

Ora esegui i test Mocha nella directory delle funzioni e scorri fino all'inizio dell'output:

# 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

Al momento abbiamo quattro errori. Man mano che crei il file di regole, puoi misurare i progressi osservando il superamento di un numero maggiore di test.

9. Accesso sicuro al carrello

I primi due errori sono i test del "carrello", che verificano che:

  • Gli utenti possono creare e aggiornare solo i propri carrelli
  • Gli utenti possono leggere solo i propri carrelli

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());
  });

Facciamo in modo che questi test vengano superati. Nell'editor, apri il file delle regole di sicurezza, firestore.rules, e aggiorna le istruzioni all'interno di 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;
    }

    // ...
  }
}

Queste regole ora consentono l'accesso in lettura e scrittura solo al proprietario del carrello.

Per verificare i dati in entrata e l'autenticazione dell'utente, utilizziamo due oggetti disponibili nel contesto di ogni regola:

10. Testare l'accesso al carrello

Emulator Suite aggiorna automaticamente le regole ogni volta che firestore.rules viene salvato. Puoi verificare che l'emulatore abbia aggiornato le regole cercando nella scheda in cui è in esecuzione l'emulatore il messaggio Rules updated:

5680da418b420226.png

Esegui nuovamente i test e verifica che i primi due siano stati superati:

$ 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

Ottimo lavoro! Ora hai ottenuto l'accesso ai carrelli. Passiamo al test successivo non riuscito.

11. Controllare il flusso "Aggiungi al carrello" nell'interfaccia utente

Al momento, anche se i proprietari del carrello possono leggere e scrivere nel carrello, non possono leggere o scrivere singoli articoli. Questo perché, sebbene i proprietari abbiano accesso al documento del carrello, non hanno accesso alla raccolta secondaria degli articoli del carrello.

Questo è uno stato non funzionante per gli utenti.

Torna all'interfaccia utente web, in esecuzione su http://127.0.0.1:5000,, e prova ad aggiungere qualcosa al carrello. Viene visualizzato un errore Permission Denied, visibile dalla console di debug, perché non abbiamo ancora concesso agli utenti l'accesso ai documenti creati nella sottoraccolta items.

12. Consenti l'accesso agli articoli del carrello

Questi due test confermano che gli utenti possono solo aggiungere articoli al proprio carrello o leggerli:

  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
    }));
  });

In questo modo possiamo scrivere una regola che consenta l'accesso se l'UID dell'utente attuale è uguale a ownerUID nel documento del carrello. Poiché non è necessario specificare regole diverse per create, update, delete, puoi utilizzare una regola write, che si applica a tutte le richieste che modificano i dati.

Aggiorna la regola per i documenti nella sottoraccolta degli elementi. Il get nella condizione legge un valore da Firestore, in questo caso il ownerUID nel documento del carrello.

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. Test dell'accesso agli articoli del carrello

Ora possiamo eseguire di nuovo il test. Scorri fino all'inizio dell'output e verifica che siano stati superati più test:

$ 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

Bene! Ora tutti i nostri test vengono superati. Abbiamo un test in attesa, ma lo vedremo tra qualche passaggio.

14. Controlla di nuovo il flusso di aggiunta al carrello

Torna al front-end web ( http://127.0.0.1:5000) e aggiungi un articolo al carrello. Questo è un passaggio importante per confermare che i nostri test e le nostre regole corrispondono alla funzionalità richiesta dal cliente. Ricorda che l'ultima volta che abbiamo provato la UI, gli utenti non sono riusciti ad aggiungere articoli al carrello.

69ad26cee520bf24.png

Il client ricarica automaticamente le regole quando viene salvato il firestore.rules. Quindi, prova ad aggiungere qualcosa al carrello.

Riepilogo

Ottimo! Hai appena migliorato la sicurezza della tua app, un passaggio essenziale per prepararla alla produzione. Se si trattasse di un'app di produzione, potremmo aggiungere questi test alla nostra pipeline di integrazione continua. In questo modo, avremo la certezza che i dati del carrello avranno questi controlli di accesso, anche se altri modificano le regole.

ba5440b193e75967.gif

Ma non è tutto.

Se continui, imparerai:

  • Come scrivere una funzione attivata da un evento Firestore
  • Come creare test che funzionino su più emulatori

15. Configura i test di Cloud Functions

Finora ci siamo concentrati sul frontend della nostra app web e sulle regole di sicurezza Firestore. Tuttavia, questa app utilizza anche Cloud Functions per mantenere aggiornato il carrello dell'utente, quindi vogliamo testare anche questo codice.

Emulator Suite semplifica il test di Cloud Functions, anche di quelle che utilizzano Cloud Firestore e altri servizi.

Nell'editor, apri il file emulators-codelab/codelab-initial-state/functions/test.js e scorri fino all'ultimo test nel file. Al momento è contrassegnato come in attesa:

//  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 () => {
    ...
  });
});

Per attivare il test, rimuovi .skip, in modo che il codice sia simile a questo:

describe("adding an item to the cart recalculates the cart total. ", () => {
  // ...

  it("should sum the cost of their items", async () => {
    ...
  });
});

Successivamente, trova la variabile REAL_FIREBASE_PROJECT_ID nella parte superiore del file e modificala con il tuo ID progetto Firebase reale:

// CHANGE THIS LINE
const REAL_FIREBASE_PROJECT_ID = "changeme";

Se hai dimenticato l'ID progetto, puoi trovarlo nelle impostazioni del progetto nella console Firebase:

d6d0429b700d2b21.png

16. Esamina i test di Functions

Poiché questo test convalida l'interazione tra Cloud Firestore e Cloud Functions, richiede una configurazione più complessa rispetto ai test dei codelab precedenti. Vediamo questo test e facciamoci un'idea di cosa si aspetta.

Creare un carrello

Cloud Functions viene eseguito in un ambiente server attendibile e può utilizzare l'autenticazione dell'account di servizio utilizzata dall'SDK Admin . Innanzitutto, inizializza un'app utilizzando initializeAdminApp anziché initializeApp. Quindi, crea un DocumentReference per il carrello a cui aggiungeremo gli articoli e inizializza il carrello:

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 });

    ...
  });

Attiva la funzione

Poi, aggiungi i documenti alla sottoraccolta items del documento del carrello per attivare la funzione. Aggiungi due elementi per assicurarti di testare l'addizione che avviene nella funzione.

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 });

    ...
    });
  });

Definisci le aspettative del test

Utilizza onSnapshot() per registrare un listener per eventuali modifiche al documento del carrello. onSnapshot() restituisce una funzione che puoi chiamare per annullare la registrazione del listener.

Per questo test, aggiungi due articoli che insieme costano 9,98 $. Poi, controlla se nel carrello sono presenti itemCount e totalPrice previsti. In questo caso, la funzione ha svolto il suo compito.

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. Esegui i test

Potresti avere ancora in esecuzione gli emulatori dei test precedenti. In caso contrario, avvia gli emulatori. Dalla riga di comando, esegui

$ firebase emulators:start --import=./seed

Apri una nuova scheda del terminale (lascia in esecuzione gli emulatori) e vai alla directory delle funzioni. Potresti averlo ancora aperto dai test delle regole di sicurezza.

$ cd functions

Ora esegui i test delle unità. Dovresti vedere 5 test in totale:

$ 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

Se esamini l'errore specifico, sembra trattarsi di un errore di timeout. Questo perché il test attende l'aggiornamento corretto della funzione, che però non avviene mai. Ora siamo pronti per scrivere la funzione per superare il test.

18. Scrivi una funzione

Per correggere questo test, devi aggiornare la funzione in functions/index.js. Sebbene parte di questa funzione sia scritta, non è completa. Ecco come appare attualmente la funzione:

// 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) {
      }
    });

La funzione imposta correttamente il riferimento al carrello, ma poi, anziché calcolare i valori di totalPrice e itemCount, li aggiorna con valori hardcoded.

Recupera e scorri l'elenco di

items raccolta secondaria

Inizializza una nuova costante, itemsSnap, in modo che sia la sottoraccolta items. Quindi, scorri tutti i documenti della raccolta.

// 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) {
      }
    });

Calcola totalPrice e itemCount

Innanzitutto, inizializziamo i valori di totalPrice e itemCount a zero.

Poi, aggiungi la logica al blocco di iterazione. Innanzitutto, verifica che l'articolo abbia un prezzo. Se l'articolo non ha una quantità specificata, lascia il valore predefinito 1. Poi aggiungi la quantità al totale parziale di itemCount. Infine, aggiungi il prezzo dell'articolo moltiplicato per la quantità al totale parziale di 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) {
      }
    });

Puoi anche aggiungere la registrazione per eseguire il debug degli stati di successo ed errore:

// 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. Esegui di nuovo i test

Nella riga di comando, assicurati che gli emulatori siano ancora in esecuzione e riesegui i test. Non è necessario riavviare gli emulatori perché rilevano automaticamente le modifiche alle funzioni. Dovresti vedere tutti i test superati:

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

Ottimo lavoro!

20. Provala utilizzando l'interfaccia utente della vetrina

Per il test finale, torna all'app web ( http://127.0.0.1:5000/) e aggiungi un articolo al carrello.

69ad26cee520bf24.png

Verifica che il carrello venga aggiornato con il totale corretto. Fantastico!

Riepilogo

Hai esaminato uno scenario di test complesso tra Cloud Functions for Firebase e Cloud Firestore. Hai scritto una funzione Cloud Functions per superare il test. Hai anche confermato che la nuova funzionalità funziona nell'interfaccia utente. Hai fatto tutto questo in locale, eseguendo gli emulatori sulla tua macchina.

Hai anche creato un client web in esecuzione sugli emulatori locali, regole di sicurezza personalizzate per proteggere i dati e testato le regole di sicurezza utilizzando gli emulatori locali.

c6a7aeb91fe97a64.gif