Test delle unità di Cloud Functions

Questa pagina descrive le best practice e gli strumenti per scrivere test delle unità per le tue funzioni, ad esempio i test che fanno parte di un sistema di integrazione continua (CI). Per semplificare i test, Firebase fornisce il 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 della rimozione appropriate per i test, ad esempio impostando e annullando le 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 al test.

Configurazione dei test

Installa firebase-functions-test e 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 simile a index.test.js.

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

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

Dopo aver scritto i test, puoi eseguirli eseguendo npm test nella 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 del 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 gli altri SDK Google utilizzati nelle funzioni.
  2. Modalità offline: scrivi test delle unità 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. In genere, l'utilizzo della modalità offline non è consigliato se hai Cloud Firestore o Realtime Database funzioni, poiché aumenta notevolmente la complessità del codice di test.

Inizializzare 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 dell'account di servizio.

Per ottenere i valori di configurazione del progetto Firebase:

  1. Apri le impostazioni del progetto nella Firebase console.
  2. In Le tue app,seleziona l'app che ti interessa.
  3. Nel riquadro a 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 di chiavi:

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

Dopo aver salvato il file di chiavi, 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' }});

Importazione delle funzioni

Per importare le funzioni, utilizza require per importare il file delle funzioni principali come modulo. Assicurati di farlo solo dopo aver inizializzato firebase-functions-test, e 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 sottoporlo a 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');

Test delle funzioni in background (non HTTP)

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

  1. Esegui il wrapping della funzione che vuoi testare con il metodo test.wrap.
  2. Crea dati di test.
  3. Richiama la funzione sottoposta a wrapping con i dati di test che hai creato e tutti i campi del contesto dell'evento che vuoi specificare.
  4. Crea asserzioni sul comportamento.

Per prima cosa, esegui il wrapping della funzione che vuoi testare. Supponiamo che tu abbia 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 accetta 2 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 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 dell'evento con campi sensibili. Puoi sostituire alcuni dei campi generati specificandoli qui. Tieni presente che devi includere solo i campi che vuoi sostituire. Tutti i campi che non hai sostituito vengono generati.
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
});

Creazione di dati di test

Il primo parametro di una funzione sottoposta a wrapping 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 creare i dati necessari per testare le funzioni. Ad esempio, utilizza test.firestore.makeDocumentSnapshot per creare un DocumentSnapshot di Firestore. Il primo argomento sono i dati, e il secondo è il percorso di riferimento completo, e c'è un terzo 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. Poi 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.

Utilizzo di 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 sottoposti a stub (per la modalità offline)

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

Supponiamo che tu stia scrivendo 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 i percorsi di codice, e utilizza Sinon per sottoporre a stub i 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);

Creazione di 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 e creare asserzioni sul comportamento. Puoi utilizzare una libreria come Chai per creare queste asserzioni.

Creazione di asserzioni in modalità online

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

L'esempio seguente verifica che "INPUT" sia 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');
  });
});

Creazione di asserzioni in modalità offline

Puoi creare 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 gli spie di Sinon per verificare che determinati metodi siano stati chiamati e con i parametri previsti.

Test delle funzioni HTTP

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

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

  • Utilizzi functions.config().
  • La 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 addMessage() funzione di esempio:

  • Sostituisci la 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. Questa funzione annulla le variabili di ambiente impostate dall'SDK durante l'inizializzazione ed elimina le app Firebase che potrebbero essere state create se hai utilizzato l'SDK per creare un DataSnapshot di Realtime Database o un DocumentSnapshot di Firestore.

test.cleanup();

Esaminare esempi completi e scoprire di più

Puoi esaminare gli esempi completi nel repository GitHub di Firebase.

Per scoprire di più, consulta il Riferimento API per firebase-functions-test.