了解 2023 年 Google I/O 大会上介绍的 Firebase 亮点。了解详情

Unit test di Cloud Functions

Questa pagina descrive le best practice e gli strumenti per la scrittura di 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. È distribuito su npm come firebase-functions-test ed è un SDK di test complementare a firebase-functions . L'SDK Firebase Test per le funzioni cloud:

  • Si occupa della configurazione e dello smontaggio appropriati per i test, come l'impostazione e la disattivazione delle variabili di ambiente necessarie per firebase-functions .
  • Genera dati di esempio e contesto dell'evento, in modo che tu debba solo specificare 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 functions:

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

Quindi crea una cartella test all'interno della cartella functions, crea un nuovo file al suo interno per il tuo codice di test e chiamalo qualcosa come 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 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 dell'utente e così via 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 silos e offline senza effetti collaterali. Ciò significa che tutte le chiamate di metodo che interagiscono con un prodotto Firebase (ad esempio la scrittura nel database o la creazione di un utente) devono essere sottoposte a stub. L'utilizzo della modalità offline è generalmente sconsigliato se si dispone di 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 l'inizializzazione dell'app tramite firebase-admin e il percorso di un file della chiave dell'account di servizio.

Per ottenere i valori di configurazione del tuo progetto Firebase:

  1. Apri le impostazioni del tuo 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 di Google Cloud Console.
  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')();

Mocking dei valori di configurazione

Se usi functions.config() nel codice delle tue funzioni, puoi prendere in giro 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 prendere in giro 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 tuo file delle funzioni principali come modulo. Assicurati di farlo solo dopo aver inizializzato firebase-functions-test e preso in giro 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 tue funzioni, devi eseguire lo stub prima di importare le tue 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).

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 i dati di prova
  3. Richiama la funzione incapsulata con i dati di test che hai creato e tutti i campi del contesto dell'evento che desideri specificare.
  4. Fai affermazioni sul comportamento.

Per prima cosa avvolgi la funzione che desideri testare. Diciamo che hai una funzione in functions/index.js chiamata makeUppercase , che vorresti 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 . Questo 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 (facoltativo): 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 evento con campi sensibili. Puoi sovrascrivere alcuni dei campi generati specificandoli qui. Tieni presente che devi includere solo i campi che desideri sovrascrivere. Tutti i campi che non hai sovrascritto 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
});

Costruzione dei dati di prova

Il primo parametro di una funzione di wrapping sono i dati di test con cui richiamare la funzione sottostante. Esistono diversi modi per costruire i 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 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 , dovrai creare due istantanee: una per lo stato precedente e una per lo stato successivo. Quindi, puoi utilizzare il metodo makeChange per creare un oggetto Change con queste istantanee.

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

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

Utilizzo di dati di esempio

Se non è necessario 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();

Vedere 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, dovresti utilizzare un oggetto semplice con stub invece di creare un vero e proprio DocumentSnapshot o DataSnapshot .

Supponiamo che tu stia scrivendo un unit test 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 questi percorsi di codice funzioneranno e usa 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, eseguito il wrapping delle funzioni e costruito i dati, è possibile richiamare le funzioni di cui è stato eseguito il wrapping con i dati costruiti e fare asserzioni sul comportamento. Puoi usare una libreria come Chai per fare queste affermazioni.

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 affermazioni sul valore di ritorno 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 alcuni metodi sono stati chiamati e con i parametri che ti aspetti.

Testare le 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, dovresti usare firebase-functions-test se:

  • Tu usi 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() :

  • Sovrascrivi la funzione di reindirizzamento nell'oggetto risposta, poiché sendMessage() lo chiama.
  • All'interno della funzione di reindirizzamento, usa chai.assert per fare affermazioni 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 cleanup. 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();

Rivedi gli esempi completi e scopri di più

Puoi esaminare gli esempi completi nel repository GitHub di Firebase.

Per saperne di più, fai riferimento al riferimento API per firebase-functions-test .