Test unitari di Cloud Functions

Questa pagina descrive le migliori pratiche e gli strumenti per scrivere unit test per le tue funzioni, come 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 a firebase-functions . L'SDK di test Firebase per Cloud Functions:

  • Si occupa della configurazione e dello smontaggio appropriati per i tuoi test, come l'impostazione e la disattivazione delle variabili di ambiente necessarie alle firebase-functions .
  • Genera dati di esempio e contesto dell'evento, in modo che tu debba specificare solo i campi rilevanti per il tuo test.

Configurazione di prova

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

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 simile index.test.js .

Infine, 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 dell'SDK di test Firebase 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 degli utenti, ecc. avvengano effettivamente e il tuo codice di test possa ispezionare i risultati. Ciò significa anche che funzioneranno anche altri SDK di Google utilizzati nelle tue funzioni.
  2. Modalità offline: scrivi unit test in silo e offline senza effetti collaterali. Ciò significa che qualsiasi chiamata al metodo che interagisce con un prodotto Firebase (ad esempio la scrittura nel database o la creazione di un utente) deve essere sottoposta a stub. L'utilizzo della modalità offline è generalmente sconsigliato se si dispone delle funzioni Cloud Firestore o Realtime Database, poiché aumenta notevolmente la complessità del codice di test.

Inizializza l'SDK in modalità online (consigliato)

Se desideri 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 chiave dell'account di servizio.

Per ottenere i valori di configurazione del tuo progetto Firebase:

  1. Apri le impostazioni del progetto nella console Firebase .
  2. In Le tue app seleziona l'app desiderata.
  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 Config per visualizzare i valori di configurazione.

Per creare un file chiave:

  1. Apri il riquadro Account di servizio della console Google Cloud.
  2. Seleziona l'account di servizio predefinito di 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 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');

Inizializza l'SDK in modalità offline

Se desideri scrivere test completamente offline, puoi inizializzare l'SDK senza alcun parametro:

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

Valori di configurazione beffardi

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

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

Quindi puoi deridere il valore all'interno del tuo file di test in questo modo:

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

Importare le tue funzioni

Per importare le tue funzioni, usa require per importare il file delle funzioni principali come modulo. Assicurati di farlo 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 eseguirne lo 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).

Il processo per testare le funzioni non HTTP prevede i seguenti passaggi:

  1. Avvolgi la funzione che desideri testare con il metodo test.wrap
  2. Costruire dati di test
  3. Richiama la funzione racchiusa con i dati di test che hai costruito e tutti i campi del contesto dell'evento che desideri specificare.
  4. Fare asserzioni sul comportamento.

Per prima cosa esegui il wrap della funzione che desideri testare. Supponiamo che tu abbia una funzione in functions/index.js chiamata makeUppercase , che desideri 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 invoca makeUppercase quando viene chiamata. wrapped richiede 2 parametri:

  1. data (obbligatorio): i dati da inviare a makeUppercase . Ciò corrisponde direttamente al primo parametro inviato al gestore di funzione che hai scritto. firebase-functions-test fornisce metodi per costruire dati personalizzati o dati di esempio.
  2. eventContextOptions (opzionale): campi del contesto dell'evento che desideri specificare. Il contesto dell'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 dell'evento con campi sensibili. Puoi sovrascrivere alcuni dei campi generati specificandoli qui. Tieni presente che devi includere solo i campi che desideri sovrascrivere. Vengono generati tutti i campi che non hai sovrascritto.
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 dei dati di test

Il primo parametro di una funzione inserita sono i dati di test con cui richiamare la funzione sottostante. Esistono diversi modi per costruire 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 Firestore DocumentSnapshot . Il primo argomento sono i dati, il secondo argomento è il percorso di riferimento completo ed è disponibile un terzo argomento facoltativo per altre proprietà dello snapshot che è possibile 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 , dovrai creare due snapshot: uno per lo stato precedente e uno per lo stato successivo. Quindi, 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.

Utilizzando dati di esempio

Se non hai bisogno di personalizzare i dati utilizzati nei tuoi test, firebase-functions-test offre metodi per generare dati di esempio per ciascun 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 dei dati stubbed (per la modalità offline)

Se hai inizializzato l'SDK in modalità offline e stai testando una funzione Cloud Firestore o Realtime Database, dovresti utilizzare un oggetto semplice con stub invece di creare un vero e proprio DocumentSnapshot o DataSnapshot .

Supponiamo che tu stia scrivendo un test unitario 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 funzioneranno entrambi questi percorsi di codice e utilizza Sinon per eseguire lo 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, aver racchiuso le funzioni e costruito i dati, è possibile richiamare le funzioni racchiuse con i dati costruiti e fare asserzioni sul comportamento. Puoi utilizzare una libreria come Chai per fare queste asserzioni.

Fare affermazioni in modalità online

Se hai inizializzato Firebase Test SDK for Cloud Functions in modalità online , puoi affermare che le azioni desiderate (come la scrittura di un 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 asserzioni sul valore restituito atteso 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 usare le spie Sinon per affermare che sono stati chiamati determinati metodi e con i parametri che ti aspetti.

Testare le funzioni HTTP

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

Se stai testando le funzioni HTTP onRequest, dovresti 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 vero progetto Firebase e le sue credenziali per i tuoi test.

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

  • Sostituisci la funzione di reindirizzamento nell'oggetto risposta, poiché sendMessage() la chiama.
  • All'interno della funzione di reindirizzamento, utilizzare chai.assert per aiutare a fare asserzioni su quali parametri dovrebbe 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);

Prova la pulizia

Alla fine del codice di test, chiama la funzione di pulizia. Ciò annulla l'impostazione delle variabili di ambiente impostate dall'SDK al momento dell'inizializzazione ed elimina 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 gli esempi completi e scopri di più

Puoi rivedere gli esempi completi nel repository GitHub di Firebase.

Per ulteriori informazioni, fare riferimento al riferimento API per firebase-functions-test .