Test delle unità di Cloud Functions

Questa pagina descrive le best practice e gli strumenti per scrivere unit test per le tue funzioni, ad esempio test che farebbero parte di un sistema di integrazione continua (CI). Per semplificare i test, Firebase fornisce Firebase Test SDK per Cloud Functions. Viene distribuito su npm come firebase-functions-test ed è un SDK di test complementare a firebase-functions. Il Firebase Test SDK per Cloud Functions:

  • Si occupa della configurazione e dell'eliminazione appropriate per i test, ad esempio l'impostazione e l'annullamento delle variabili di ambiente necessarie per firebase-functions.
  • Genera dati di esempio e contesto dell'evento, in modo che tu debba specificare solo i campi pertinenti per il test.

Configurazione del test

Installa sia firebase-functions-test che Mocha, un framework di test, eseguendo i seguenti comandi nella cartella delle funzioni:

npm install --save-dev firebase-functions-test
npm install --save-dev mocha

Poi crea una cartella test all'interno della cartella delle funzioni, crea un nuovo file al suo interno per il codice di test e assegnagli un nome come index.test.js.

Infine, modifica functions/package.json per aggiungere quanto segue:

"scripts": {
  "test": "mocha --reporter spec"
}

Una volta scritti i test, puoi eseguirli eseguendo npm test all'interno della directory delle funzioni.

Inizializzazione di Firebase Test SDK per Cloud Functions

Esistono due modi per utilizzare firebase-functions-test:

  1. Modalità online (consigliata): scrivi test che interagiscono con un progetto Firebase dedicato ai test, in modo che le scritture nel database, le creazioni di utenti e così via avvengano effettivamente e il codice di test possa esaminare i risultati. Ciò significa anche che funzioneranno anche altri SDK Google utilizzati nelle tue funzioni.
  2. Modalità offline:scrivi test unitari isolati e offline senza effetti collaterali. Ciò significa che tutte le chiamate di metodi che interagiscono con un prodotto Firebase (ad es. la scrittura nel database o la creazione di un utente) devono essere sottoposte a stub. L'utilizzo della modalità offline non è generalmente consigliato se hai funzioni Cloud Firestore o Realtime Database, in quanto aumenta notevolmente la complessità del codice di test.

Inizializza l'SDK in modalità online (consigliata)

Se vuoi scrivere test che interagiscono con un progetto di test, devi fornire i valori di configurazione del progetto necessari per inizializzare l'app tramite firebase-admin e il percorso di un file di chiavi del service account.

Per ottenere i valori di configurazione del progetto Firebase:

  1. Apri le impostazioni del progetto nella console Firebase.
  2. In Le tue app,seleziona l'app che ti interessa.
  3. Nel riquadro di destra, seleziona l'opzione per scaricare un file di configurazione per le app Apple e Android.

    Per le app web, seleziona Configurazione per visualizzare i valori di configurazione.

Per creare un file della chiave:

  1. Apri il riquadro Account di servizio della console Google Cloud.
  2. Seleziona il service account predefinito App Engine e utilizza il menu delle opzioni a destra per selezionare Crea chiave.
  3. Quando richiesto, seleziona JSON come tipo di chiave e fai clic su Crea.

Dopo aver salvato il file di chiave, inizializza l'SDK:

// At the top of test/index.test.js
// Make sure to use values from your actual Firebase configuration
const test = require('firebase-functions-test')({
  databaseURL: 'https://PROJECT_ID.firebaseio.com',
  storageBucket: 'PROJECT_ID.firebasestorage.app',
  projectId: 'PROJECT_ID',
}, 'path/to/serviceAccountKey.json');

Inizializzare l'SDK in modalità offline

Se vuoi scrivere test completamente offline, puoi inizializzare l'SDK senza parametri:

// At the top of test/index.test.js
const test = require('firebase-functions-test')();

Simulazione dei valori di configurazione

Se utilizzi functions.config() nel codice delle funzioni, puoi simulare i valori di configurazione. Ad esempio, se functions/index.js contiene il seguente codice:

const functions = require('firebase-functions/v1');
const key = functions.config().stripe.key;

Poi puoi simulare il valore all'interno del file di test nel seguente modo:

// Mock functions config values
test.mockConfig({ stripe: { key: '23wr42ewr34' }});

Importare le funzioni

Per importare le funzioni, utilizza require per importare il file delle funzioni principali come modulo. Assicurati di eseguire questa operazione solo dopo aver inizializzato firebase-functions-test e aver simulato i valori di configurazione.

// after firebase-functions-test has been initialized
const myFunctions = require('../index.js'); // relative path to functions code

Se hai inizializzato firebase-functions-test in modalità offline e hai admin.initializeApp() nel codice delle funzioni, devi creare uno stub prima di importare le funzioni:

// If index.js calls admin.initializeApp at the top of the file,
// we need to stub it out before requiring index.js. This is because the
// functions will be executed as a part of the require process.
// Here we stub admin.initializeApp to be a dummy function that doesn't do anything.
adminInitStub = sinon.stub(admin, 'initializeApp');
// Now we can require index.js and save the exports inside a namespace called myFunctions.
myFunctions = require('../index');

Testare le funzioni in background (non HTTP)

La procedura per testare le funzioni non HTTP prevede i seguenti passaggi:

  1. Racchiudi la funzione che vuoi testare con il metodo test.wrap
  2. Costruire dati di test
  3. Richiama la funzione wrapper con i dati di test che hai creato e tutti i campi di contesto dell'evento che vuoi specificare.
  4. Fare affermazioni sul comportamento.

Innanzitutto, esegui il wrapping della funzione che vuoi testare. Supponiamo di avere una funzione in functions/index.js chiamata makeUppercase, che vuoi testare. Scrivi quanto segue in functions/test/index.test.js

// "Wrap" the makeUpperCase function from index.js
const myFunctions = require('../index.js');
const wrapped = test.wrap(myFunctions.makeUppercase);

wrapped è una funzione che richiama makeUppercase quando viene chiamata. wrapped richiede due parametri:

  1. data (obbligatorio): i dati da inviare a makeUppercase. Corrisponde direttamente al primo parametro inviato al gestore di funzioni che hai scritto. firebase-functions-test fornisce metodi per creare dati personalizzati o dati di esempio.
  2. eventContextOptions (facoltativo): i campi del contesto dell'evento che vuoi specificare. Il contesto dell'evento è il secondo parametro inviato al gestore di funzioni che hai scritto. Se non includi un parametro eventContextOptions quando chiami wrapped, viene comunque generato un contesto evento con campi sensibili. Puoi sovrascrivere alcuni dei campi generati specificandoli qui. Tieni presente che devi includere solo i campi che vuoi sostituire. Vengono generati tutti i campi che non hai sostituito.
const data =  // See next section for constructing test data

// Invoke the wrapped function without specifying the event context.
wrapped(data);

// Invoke the function, and specify params
wrapped(data, {
  params: {
    pushId: '234234'
  }
});

// Invoke the function, and specify auth and auth Type (for real time database functions only)
wrapped(data, {
  auth: {
    uid: 'jckS2Q0'
  },
  authType: 'USER'
});

// Invoke the function, and specify all the fields that can be specified
wrapped(data, {
  eventId: 'abc',
  timestamp: '2018-03-23T17:27:17.099Z',
  params: {
    pushId: '234234'
  },
  auth: {
    uid: 'jckS2Q0' // only for real time database functions
  },
  authType: 'USER' // only for real time database functions
});

Costruzione di dati di test

Il primo parametro di una funzione wrapper sono i dati di test con cui richiamare la funzione sottostante. Esistono diversi modi per creare dati di test.

Utilizzo di dati personalizzati

firebase-functions-test ha una serie di funzioni per costruire i dati necessari per testare le tue funzioni. Ad esempio, utilizza test.firestore.makeDocumentSnapshot per creare un DocumentSnapshot Firestore. Il primo argomento sono i dati, il secondo è il percorso di riferimento completo e il terzo è un argomento facoltativo per altre proprietà dello snapshot che puoi specificare.

// Make snapshot
const snap = test.firestore.makeDocumentSnapshot({foo: 'bar'}, 'document/path');
// Call wrapped function with the snapshot
const wrapped = test.wrap(myFunctions.myFirestoreDeleteFunction);
wrapped(snap);

Se stai testando una funzione onUpdate o onWrite, devi creare due snapshot: uno per lo stato precedente e uno per lo stato successivo. A questo punto, puoi utilizzare il metodo makeChange per creare un oggetto Change con questi snapshot.

// Make snapshot for state of database beforehand
const beforeSnap = test.firestore.makeDocumentSnapshot({foo: 'bar'}, 'document/path');
// Make snapshot for state of database after the change
const afterSnap = test.firestore.makeDocumentSnapshot({foo: 'faz'}, 'document/path');
const change = test.makeChange(beforeSnap, afterSnap);
// Call wrapped function with the Change object
const wrapped = test.wrap(myFunctions.myFirestoreUpdateFunction);
wrapped(change);

Consulta il riferimento API per funzioni simili per tutti gli altri tipi di dati.

Utilizzare i dati di esempio

Se non devi personalizzare i dati utilizzati nei test, firebase-functions-test offre metodi per generare dati di esempio per ogni tipo di funzione.

// For Firestore onCreate or onDelete functions
const snap = test.firestore.exampleDocumentSnapshot();
// For Firestore onUpdate or onWrite functions
const change = test.firestore.exampleDocumentSnapshotChange();

Consulta il riferimento API per i metodi per ottenere dati di esempio per ogni tipo di funzione.

Utilizzo di dati stub (per la modalità offline)

Se hai inizializzato l'SDK in modalità offline e stai testando una funzione Cloud Firestore o Realtime Database, devi utilizzare un oggetto semplice con stub anziché creare un DocumentSnapshot o DataSnapshot effettivo.

Supponiamo di scrivere un test delle unità per la seguente funzione:

// Listens for new messages added to /messages/:pushId/original and creates an
// uppercase version of the message to /messages/:pushId/uppercase
exports.makeUppercase = functions.database.ref('/messages/{pushId}/original')
    .onCreate((snapshot, context) => {
      // Grab the current value of what was written to the Realtime Database.
      const original = snapshot.val();
      functions.logger.log('Uppercasing', context.params.pushId, original);
      const uppercase = original.toUpperCase();
      // You must return a Promise when performing asynchronous tasks inside a Functions such as
      // writing to the Firebase Realtime Database.
      // Setting an "uppercase" sibling in the Realtime Database returns a Promise.
      return snapshot.ref.parent.child('uppercase').set(uppercase);
    });

All'interno della funzione, snap viene utilizzato due volte:

  • snap.val()
  • snap.ref.parent.child('uppercase').set(uppercase)

Nel codice di test, crea un oggetto semplice in cui funzionino entrambi questi percorsi di codice e usa Sinon per creare stub dei metodi.

// The following lines creates a fake snapshot, 'snap', which returns 'input' when snap.val() is called,
// and returns true when snap.ref.parent.child('uppercase').set('INPUT') is called.
const snap = {
  val: () => 'input',
  ref: {
    parent: {
      child: childStub,
    }
  }
};
childStub.withArgs(childParam).returns({ set: setStub });
setStub.withArgs(setParam).returns(true);

Fare asserzioni

Dopo aver inizializzato l'SDK, eseguito il wrapping delle funzioni e creato i dati, puoi richiamare le funzioni sottoposte a wrapping con i dati creati ed eseguire asserzioni sul comportamento. Puoi utilizzare una libreria come Chai per effettuare queste asserzioni.

Creazione di asserzioni in modalità online

Se hai inizializzato Firebase Test SDK per Cloud Functions in modalità online, puoi verificare che le azioni desiderate (ad esempio una scrittura nel database) siano state eseguite utilizzando l'SDK firebase-admin.

L'esempio seguente afferma che "INPUT" è stato scritto nel database del progetto di test.

// Create a DataSnapshot with the value 'input' and the reference path 'messages/11111/original'.
const snap = test.database.makeDataSnapshot('input', 'messages/11111/original');

// Wrap the makeUppercase function
const wrapped = test.wrap(myFunctions.makeUppercase);
// Call the wrapped function with the snapshot you constructed.
return wrapped(snap).then(() => {
  // Read the value of the data at messages/11111/uppercase. Because `admin.initializeApp()` is
  // called in functions/index.js, there's already a Firebase app initialized. Otherwise, add
  // `admin.initializeApp()` before this line.
  return admin.database().ref('messages/11111/uppercase').once('value').then((createdSnap) => {
    // Assert that the value is the uppercased version of our input.
    assert.equal(createdSnap.val(), 'INPUT');
  });
});

Fare asserzioni in modalità offline

Puoi fare asserzioni sul valore restituito previsto della funzione:

const childParam = 'uppercase';
const setParam = 'INPUT';
// Stubs are objects that fake and/or record function calls.
// These are excellent for verifying that functions have been called and to validate the
// parameters passed to those functions.
const childStub = sinon.stub();
const setStub = sinon.stub();
// The following lines creates a fake snapshot, 'snap', which returns 'input' when snap.val() is called,
// and returns true when snap.ref.parent.child('uppercase').set('INPUT') is called.
const snap = {
  val: () => 'input',
  ref: {
    parent: {
      child: childStub,
    }
  }
};
childStub.withArgs(childParam).returns({ set: setStub });
setStub.withArgs(setParam).returns(true);
// Wrap the makeUppercase function.
const wrapped = test.wrap(myFunctions.makeUppercase);
// Since we've stubbed snap.ref.parent.child(childParam).set(setParam) to return true if it was
// called with the parameters we expect, we assert that it indeed returned true.
return wrapped(snap).then(makeUppercaseResult => {
  return assert.equal(makeUppercaseResult, true);
});

Puoi anche utilizzare Sinon spies per verificare che determinati metodi siano stati chiamati e con i parametri che ti aspetti.

Test delle funzioni HTTP

Per testare le funzioni HTTP onCall, utilizza lo stesso approccio del test delle funzioni in background.

Se stai testando le funzioni HTTP onRequest, devi utilizzare firebase-functions-test se:

  • Utilizzi functions.config()
  • La tua funzione interagisce con un progetto Firebase o altre API di Google e vuoi utilizzare un progetto Firebase reale e le relative credenziali per i test.

Una funzione HTTP onRequest accetta due parametri: un oggetto richiesta e un oggetto risposta. Ecco come potresti testare la funzione di esempio addMessage():

  • Esegui l'override della funzione di reindirizzamento nell'oggetto risposta, poiché sendMessage() la chiama.
  • All'interno della funzione di reindirizzamento, utilizza chai.assert per creare asserzioni sui parametri con cui deve essere chiamata la funzione di reindirizzamento:
// A fake request object, with req.query.text set to 'input'
const req = { query: {text: 'input'} };
// A fake response object, with a stubbed redirect function which asserts that it is called
// with parameters 303, 'new_ref'.
const res = {
  redirect: (code, url) => {
    assert.equal(code, 303);
    assert.equal(url, 'new_ref');
    done();
  }
};

// Invoke addMessage with our fake request and response objects. This will cause the
// assertions in the response object to be evaluated.
myFunctions.addMessage(req, res);

Pulizia dei test

Alla fine del codice di test, chiama la funzione di pulizia. In questo modo vengono annullate le variabili di ambiente impostate dall'SDK durante l'inizializzazione e vengono eliminate le app Firebase che potrebbero essere state create se hai utilizzato l'SDK per creare un database in tempo reale DataSnapshot o Firestore DocumentSnapshot.

test.cleanup();

Esaminare esempi completi e scoprire di più

Puoi esaminare gli esempi completi nel repository GitHub di Firebase.

Per saperne di più, consulta il riferimento API per firebase-functions-test.