Test delle unità di Cloud Functions

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

  • Gestisce la configurazione e lo smantellamento appropriati per i test, ad esempio l'impostazione e la disattivazione delle variabili di ambiente necessarie per firebase-functions.
  • Genera dati di esempio e contesto evento, in modo da dover specificare solo i campi pertinenti per il test.

Configurazione del test

Installa sia firebase-functions-test sia 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

Successivamente, 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 aggiungendo 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 del database, le creazioni di utenti e così via vengano effettivamente eseguite e il codice di test possa ispezionare i risultati. Ciò significa anche che funzioneranno anche gli altri SDK Google utilizzati nelle tue funzioni.
  2. Modalità offline:scrivi test di unità silo e offline senza effetti collaterali. Ciò significa che tutte le chiamate ai metodi che interagiscono con un prodotto Firebase (ad es. scrittura nel database o creazione di un utente) devono essere simulate. In genere, l'utilizzo della modalità offline non è consigliato se hai funzioni Cloud Firestore o Realtime Database, poiché aumenta notevolmente la complessità del codice di test.

Inizializza l'SDK in modalità online (opzione consigliata)

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

Per recuperare 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 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 della chiave:

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

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

// At the top of test/index.test.js
const test = require('firebase-functions-test')({
  databaseURL: 'https://my-project.firebaseio.com',
  storageBucket: 'my-project.appspot.com',
  projectId: 'my-project',
}, '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 eseguire questa operazione 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 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');

Test delle funzioni in background (non HTTP)

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

  1. Inserisci la funzione da testare nel metodo test.wrap
  2. Crea i dati di test
  3. Richiama la funzione con wrapping con i dati di test che hai creato e con eventuali campi del contesto dell'evento da specificare.
  4. Effettuare affermazioni sul comportamento.

Innanzitutto, racchiudi la 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 richiede due parametri:

  1. data (obbligatorio): i dati da inviare a makeUppercase. Corrisponde direttamente al primo parametro inviato all'handler della funzione che hai scritto. firebase-functions-test fornisce metodi per la creazione di dati personalizzati o di esempio.
  2. eventContextOptions (facoltativo): i campi del contesto dell'evento da specificare. Il contesto evento è il secondo parametro inviato al gestore della funzione che hai scritto. Se non includi un parametro eventContextOptions quando chiami wrapped, viene comunque generato un contesto 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 dei dati di test

Il primo parametro di una funzione con wrapping è costituito dai dati di test con cui invocare la funzione di base. Esistono diversi modi per creare dati di test.

Utilizzo di dati personalizzati

firebase-functions-test dispone di una serie di funzioni per la costruzione dei dati necessari per testare le funzioni. Ad esempio, utilizza test.firestore.makeDocumentSnapshot per creare un DocumentSnapshot Firestore. Il primo argomento è costituito dai dati, il secondo è il percorso di riferimento completo e esiste un terzo argomento facoltativo per altre proprietà dell'istantanea 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 istantanee: una per lo stato precedente e una per lo stato successivo. Puoi quindi utilizzare il metodo makeChange per creare un oggetto Change con questi istantanei.

// 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 all'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 all'API per i metodi per recuperare i dati di esempio per ogni tipo di funzione.

Utilizzo di dati simulati (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 normale con stub instead of creating an actual DocumentSnapshot or 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 entrambi i percorsi di codice funzioneranno e utilizza Sinon per creare uno 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 affermazioni

Dopo aver inizializzato l'SDK, avvolto le funzioni e costruito i dati, puoi invocare le funzioni avvolte con i dati costruiti ed eseguire asserzioni sul comportamento. Puoi utilizzare una libreria come Chai per effettuare queste affermazioni.

Fare affermazioni in modalità online

Se hai inizializzato Firebase Test SDK per Cloud Functions in modalità online, puoi affermare che le azioni desiderate (ad esempio una scrittura nel database) sono 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 affermazioni in modalità offline

Puoi fare affermazioni 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 assert.equal(wrapped(snap), true);

Puoi anche utilizzare gli spia di Sinon per verificare che determinati metodi siano stati chiamati e con i parametri previsti.

Test delle funzioni HTTP

Per testare le funzioni onCall HTTP, utilizza lo stesso approccio utilizzato per testare le funzioni in background.

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

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

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

  • Sostituisci la funzione di reindirizzamento nell'oggetto di risposta, poiché sendMessage() la chiama.
  • All'interno della funzione di reindirizzamento, utilizza chai.assert per fare affermazioni 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 del test

All'estremità del codice di test, chiama la funzione di pulizia. In questo modo, vengono annullate le variabili di ambiente impostate dall'SDK al momento dell'inizializzazione ed 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();

Esamina esempi completi e scopri di più

Puoi esaminare gli esempi completi nel repository GitHub di Firebase.

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