Unit-Tests von Cloud-Funktionen

Auf dieser Seite werden Best Practices und Tools zum Schreiben von Unit-Tests für Ihre Funktionen beschrieben, z. B. Tests, die Teil eines Continuous Integration (CI)-Systems wären. Um das Testen zu vereinfachen, stellt Firebase das Firebase Test SDK für Cloud Functions bereit. Es wird auf npm als firebase-functions-test verteilt und ist ein begleitendes Test-SDK zu firebase-functions . Das Firebase Test SDK für Cloud Functions:

  • Kümmert sich um die entsprechende Einrichtung und Demontage Ihrer Tests, z. B. das Setzen und Deaktivieren von Umgebungsvariablen, die von firebase-functions benötigt werden.
  • Generiert Beispieldaten und Ereigniskontext, sodass Sie nur die Felder angeben müssen, die für Ihren Test relevant sind.

Versuchsaufbau

Installieren Sie sowohl firebase-functions-test als auch Mocha , ein Testframework, indem Sie die folgenden Befehle in Ihrem Funktionsordner ausführen:

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

Erstellen Sie als Nächstes einen test im Funktionsordner, erstellen Sie darin eine neue Datei für Ihren Testcode und nennen Sie sie etwa index.test.js .

Ändern Sie abschließend functions/package.json , um Folgendes hinzuzufügen:

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

Sobald Sie die Tests geschrieben haben, können Sie sie ausführen, indem Sie npm test in Ihrem Funktionsverzeichnis ausführen.

Initialisierung des Firebase Test SDK für Cloud Functions

Es gibt zwei Möglichkeiten, firebase-functions-test zu verwenden:

  1. Online-Modus (empfohlen): Schreiben Sie Tests, die mit einem Firebase-Projekt interagieren, das dem Testen gewidmet ist, sodass Datenbankschreibvorgänge, Benutzererstellungen usw. tatsächlich stattfinden und Ihr Testcode die Ergebnisse überprüfen kann. Dies bedeutet auch, dass andere in Ihren Funktionen verwendete Google SDKs ebenfalls funktionieren.
  2. Offline-Modus: Schreiben Sie isolierte und Offline-Komponententests ohne Nebenwirkungen. Das bedeutet, dass alle Methodenaufrufe, die mit einem Firebase-Produkt interagieren (z. B. in die Datenbank schreiben oder einen Benutzer erstellen), blockiert werden müssen. Die Verwendung des Offline-Modus wird im Allgemeinen nicht empfohlen, wenn Sie über Cloud Firestore- oder Realtime Database-Funktionen verfügen, da dies die Komplexität Ihres Testcodes erheblich erhöht.

SDK im Online-Modus initialisieren (empfohlen)

Wenn Sie Tests schreiben möchten, die mit einem Testprojekt interagieren, müssen Sie die Projektkonfigurationswerte angeben, die zum Initialisieren der App über firebase-admin erforderlich sind, sowie den Pfad zu einer Dienstkontoschlüsseldatei.

So erhalten Sie die Konfigurationswerte Ihres Firebase-Projekts:

  1. Öffnen Sie Ihre Projekteinstellungen in der Firebase-Konsole .
  2. Wählen Sie unter Ihre Apps die gewünschte App aus.
  3. Wählen Sie im rechten Bereich die Option zum Herunterladen einer Konfigurationsdatei für Apple- und Android-Apps aus.

    Wählen Sie für Web-Apps „Config“ aus, um Konfigurationswerte anzuzeigen.

So erstellen Sie eine Schlüsseldatei:

  1. Öffnen Sie den Bereich „Dienstkonten“ der Google Cloud Console.
  2. Wählen Sie das App Engine-Standarddienstkonto aus und wählen Sie im Optionsmenü rechts die Option Schlüssel erstellen aus.
  3. Wenn Sie dazu aufgefordert werden, wählen Sie JSON als Schlüsseltyp aus und klicken Sie auf Erstellen .

Initialisieren Sie nach dem Speichern der Schlüsseldatei das 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');

Initialisieren Sie das SDK im Offline-Modus

Wenn Sie vollständige Offline-Tests schreiben möchten, können Sie das SDK ohne Parameter initialisieren:

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

Konfigurationswerte verspotten

Wenn Sie functions.config() in Ihrem Funktionscode verwenden, können Sie die Konfigurationswerte verspotten. Wenn zum Beispiel functions/index.js den folgenden Code enthält:

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

Dann können Sie den Wert in Ihrer Testdatei wie folgt verspotten:

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

Importieren Sie Ihre Funktionen

Um Ihre Funktionen zu importieren, verwenden Sie require , um Ihre Hauptfunktionsdatei als Modul zu importieren. Stellen Sie sicher, dass Sie dies erst tun, nachdem Sie firebase-functions-test initialisiert und Konfigurationswerte verspottet haben.

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

Wenn Sie firebase-functions-test im Offline-Modus initialisiert haben und admin.initializeApp() in Ihrem Funktionscode enthalten ist, müssen Sie es vor dem Importieren Ihrer Funktionen stubben:

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

Hintergrundfunktionen (nicht HTTP) testen

Der Prozess zum Testen von Nicht-HTTP-Funktionen umfasst die folgenden Schritte:

  1. Umschließen Sie die Funktion, die Sie testen möchten, mit der Methode test.wrap
  2. Testdaten erstellen
  3. Rufen Sie die umschlossene Funktion mit den von Ihnen erstellten Testdaten und allen Ereigniskontextfeldern auf, die Sie angeben möchten.
  4. Machen Sie Aussagen über das Verhalten.

Umschließen Sie zunächst die Funktion, die Sie testen möchten. Nehmen wir an, Sie haben in functions/index.js eine Funktion namens makeUppercase , die Sie testen möchten. Schreiben Sie Folgendes 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 ist eine Funktion, die beim Aufruf makeUppercase aufruft. wrapped benötigt zwei Parameter:

  1. data (erforderlich): die Daten, die an makeUppercase gesendet werden sollen. Dies entspricht direkt dem ersten Parameter, der an den von Ihnen geschriebenen Funktionshandler gesendet wird. firebase-functions-test bietet Methoden zum Erstellen benutzerdefinierter Daten oder Beispieldaten.
  2. eventContextOptions (optional): Felder des Ereigniskontexts, den Sie angeben möchten. Der Ereigniskontext ist der zweite Parameter, der an den von Ihnen geschriebenen Funktionshandler gesendet wird. Wenn Sie beim Aufruf von wrapped keinen eventContextOptions Parameter angeben, wird dennoch ein Ereigniskontext mit sinnvollen Feldern generiert. Sie können einige der generierten Felder überschreiben, indem Sie sie hier angeben. Beachten Sie, dass Sie nur die Felder einschließen müssen, die Sie überschreiben möchten. Alle Felder, die Sie nicht überschrieben haben, werden generiert.
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
});

Testdaten erstellen

Der erste Parameter einer umschlossenen Funktion sind die Testdaten, mit denen die zugrunde liegende Funktion aufgerufen wird. Es gibt verschiedene Möglichkeiten, Testdaten zu erstellen.

Verwendung benutzerdefinierter Daten

firebase-functions-test verfügt über eine Reihe von Funktionen zum Erstellen von Daten, die zum Testen Ihrer Funktionen erforderlich sind. Verwenden Sie beispielsweise test.firestore.makeDocumentSnapshot , um einen Firestore DocumentSnapshot zu erstellen. Das erste Argument sind die Daten und das zweite Argument ist der vollständige Referenzpfad. Außerdem gibt es ein optionales drittes Argument für andere Eigenschaften des Snapshots, die Sie angeben können.

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

Wenn Sie eine onUpdate oder onWrite Funktion testen, müssen Sie zwei Snapshots erstellen: einen für den Vorher-Zustand und einen für den Nachher-Zustand. Anschließend können Sie mit der Methode makeChange ein Change Objekt mit diesen Snapshots erstellen.

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

Ähnliche Funktionen für alle anderen Datentypen finden Sie in der API-Referenz .

Anhand von Beispieldaten

Wenn Sie die in Ihren Tests verwendeten Daten nicht anpassen müssen, bietet firebase-functions-test Methoden zum Generieren von Beispieldaten für jeden Funktionstyp.

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

Methoden zum Abrufen von Beispieldaten für jeden Funktionstyp finden Sie in der API-Referenz .

Verwenden von Stub-Daten (für den Offline-Modus)

Wenn Sie das SDK im Offline-Modus initialisiert haben und eine Cloud Firestore- oder Realtime Database-Funktion testen, sollten Sie ein einfaches Objekt mit Stubs verwenden, anstatt einen tatsächlichen DocumentSnapshot oder DataSnapshot zu erstellen.

Nehmen wir an, Sie schreiben einen Komponententest für die folgende Funktion:

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

Innerhalb der Funktion wird snap zweimal verwendet:

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

Erstellen Sie im Testcode ein einfaches Objekt, in dem beide Codepfade funktionieren, und verwenden Sie Sinon , um die Methoden zu unterbrechen.

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

Behauptungen aufstellen

Nachdem Sie das SDK initialisiert, die Funktionen verpackt und Daten erstellt haben, können Sie die verpackten Funktionen mit den erstellten Daten aufrufen und Aussagen zum Verhalten treffen. Sie können eine Bibliothek wie Chai verwenden, um diese Behauptungen aufzustellen.

Aussagen im Online-Modus treffen

Wenn Sie das Firebase Test SDK für Cloud Functions im Onlinemodus initialisiert haben, können Sie mit dem firebase-admin SDK bestätigen, dass die gewünschten Aktionen (z. B. ein Datenbankschreibvorgang) stattgefunden haben.

Im folgenden Beispiel wird behauptet, dass „INPUT“ in die Datenbank des Testprojekts geschrieben wurde.

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

Behauptungen im Offline-Modus aufstellen

Sie können Aussagen über den erwarteten Rückgabewert der Funktion machen:

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

Sie können Sinon-Spione auch verwenden, um sicherzustellen, dass bestimmte Methoden aufgerufen wurden, und zwar mit den von Ihnen erwarteten Parametern.

Testen von HTTP-Funktionen

Um HTTP-onCall-Funktionen zu testen, verwenden Sie denselben Ansatz wie beim Testen von Hintergrundfunktionen .

Wenn Sie HTTP-onRequest-Funktionen testen, sollten Sie firebase-functions-test verwenden, wenn:

  • Sie verwenden functions.config()
  • Ihre Funktion interagiert mit einem Firebase-Projekt oder anderen Google-APIs und Sie möchten für Ihre Tests ein echtes Firebase-Projekt und seine Anmeldeinformationen verwenden.

Eine HTTP-onRequest-Funktion benötigt zwei Parameter: ein Anforderungsobjekt und ein Antwortobjekt. So können Sie die Beispielfunktion addMessage() testen:

  • Überschreiben Sie die Umleitungsfunktion im Antwortobjekt, da sendMessage() sie aufruft.
  • Verwenden Sie innerhalb der Umleitungsfunktion chai.assert , um Aussagen darüber zu treffen, mit welchen Parametern die Umleitungsfunktion aufgerufen werden soll:
// 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);

Probereinigung

Rufen Sie ganz am Ende Ihres Testcodes die Bereinigungsfunktion auf. Dadurch werden Umgebungsvariablen zurückgesetzt, die das SDK bei der Initialisierung festgelegt hat, und Firebase-Apps werden gelöscht, die möglicherweise erstellt wurden, wenn Sie das SDK zum Erstellen einer Echtzeitdatenbank DataSnapshot oder Firestore DocumentSnapshot verwendet haben.

test.cleanup();

Sehen Sie sich vollständige Beispiele an und erfahren Sie mehr

Sie können die vollständigen Beispiele im Firebase-GitHub-Repository einsehen.

Weitere Informationen finden Sie in der API-Referenz für firebase-functions-test .