Unittests für Cloud Functions

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

  • Übernimmt die entsprechende Einrichtung und den Abbau für Ihre Tests, z. B. das Festlegen und Aufheben von Umgebungsvariablen, die für firebase-functions erforderlich sind.
  • Generiert Beispieldaten und Ereigniskontext, sodass Sie nur die Felder angeben müssen, die für Ihren Test relevant sind.

Testkonfiguration

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

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

Erstellen Sie als Nächstes im Ordner „functions“ einen Ordner mit dem Namen test und darin eine neue Datei für Ihren Testcode, z. B. index.test.js.

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

"scripts": {
  "test": "jest"
}

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

Firebase Test SDK wird für Cloud Functions initialisiert

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

  1. Onlinemodus (empfohlen): Schreiben Sie Tests, die mit einem Firebase-Projekt interagieren, das für Tests vorgesehen ist, sodass Datenbank-Schreibvorgänge, Nutzererstellungen usw. tatsächlich stattfinden und Ihr Testcode die Ergebnisse prüfen kann. Das bedeutet, dass auch andere Google SDKs, die in Ihren Funktionen verwendet werden, funktionieren.
  2. Offlinemodus: Schreiben Sie isolierte und Offline-Unittests ohne Nebenwirkungen. Das bedeutet, dass alle Methodenaufrufe, die mit einem Firebase-Produkt interagieren (z.B. Schreiben in die Datenbank oder Erstellen eines Nutzers), gestubbt werden müssen. Die Verwendung des Offlinemodus wird im Allgemeinen nicht empfohlen, wenn Sie Cloud Firestore- oder Realtime Database-Funktionen haben, da dies die Komplexität Ihres Testcodes erheblich erhöht.

SDK im Onlinemodus initialisieren (empfohlen)

Wenn Sie Tests schreiben möchten, die mit einem Testprojekt interagieren, müssen Sie die Firebase-Konfigurationswerte, die für die Initialisierung der App über firebase-admin erforderlich sind, und den Pfad zu einer Dienstkontoschlüsseldatei angeben.

So rufen Sie Ihre Firebase-Konfigurationswerte ab:

  1. Rufen Sie in der Firebase Console die Seite Einstellungen > Allgemein auf.

  2. Scrollen Sie zur Karte Meine Apps und wählen Sie die gewünschte App aus.

  3. Rufen Sie Ihre Firebase-Konfiguration ab:

    • Wählen Sie für Apple- und Android-Apps die Option zum Herunterladen einer Konfigurationsdatei aus.

    • Wählen Sie für Web-Apps Konfiguration aus, um die Konfigurationswerte anzuzeigen.

So erstellen Sie eine Schlüsseldatei:

  1. Rufen Sie in der Google Cloud Console den Bereich Dienstkonten auf.

  2. Wählen Sie das Standarddienstkonto App Engine aus und wählen Sie im Optionsmenü rechts Schlüssel erstellen aus.

  3. Wählen Sie bei Aufforderung „JSON“ als Schlüsseltyp aus und klicken Sie auf Erstellen.

Initialisieren Sie das SDK, nachdem Sie die Schlüsseldatei gespeichert haben:

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

SDK im Offlinemodus initialisieren

Wenn Sie Tests vollständig offline 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 simulieren

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

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

Anschließend können Sie den Wert in Ihrer Testdatei so simulieren:

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

Funktionen importieren

Verwenden Sie require, um die Hauptfunktionsdatei als Modul zu importieren. Führen Sie diesen Schritt erst aus, nachdem Sie firebase-functions-test initialisiert und die Konfigurationswerte simuliert haben.

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

Wenn Sie firebase-functions-test im Offlinemodus initialisiert haben und admin.initializeApp() in Ihrem Funktionscode enthalten ist, müssen Sie es vor dem Importieren Ihrer Funktionen stubben. Fordern Sie in Ihrer Testdatei firebase-admin als admin an und ersetzen Sie dann initializeApp() durch ein Jest-Mock:

// If initializeApp() is called in index.js, we mock it out before requiring index.js
adminInitStub = jest.spyOn(admin, 'initializeApp').mockImplementation(() => {});
// Now we can require index.js and save the exports inside a namespace called myFunctions.
myFunctions = require('../index');

Hintergrundfunktionen (Nicht-HTTP-Funktionen) testen

Das Testen von Nicht-HTTP-Funktionen umfasst die folgenden Schritte:

  1. Verpacken Sie die Funktion, die Sie testen möchten, mit der Methode test.wrap.
  2. Testdaten erstellen
  3. Rufen Sie die verpackte Funktion mit den von Ihnen erstellten Testdaten und allen Ereigniskontextfeldern auf, die Sie angeben möchten.
  4. Behauptungen über das Verhalten aufstellen

Verpacken Sie zuerst die Funktion, die Sie testen möchten. Angenommen, 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 makeUppercase aufruft, wenn sie aufgerufen wird. wrapped hat 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 Funktions-Handler gesendet wird. firebase-functions-test bietet Methoden zum Erstellen benutzerdefinierter Daten oder Beispieldaten.
  2. eventContextOptions (optional): Felder des Ereigniskontexts, die Sie angeben möchten. Der Ereigniskontext ist der zweite Parameter, der an den von Ihnen geschriebenen Funktions-Handler gesendet wird. Wenn Sie beim Aufrufen von wrapped keinen eventContextOptions-Parameter angeben, wird trotzdem ein Ereigniskontext mit sinnvollen Feldern generiert. Sie können einige der generierten Felder überschreiben, indem Sie sie hier angeben. Sie müssen nur die Felder angeben, 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 verpackten Funktion sind die Testdaten, mit denen die zugrunde liegende Funktion aufgerufen werden soll. Es gibt verschiedene Möglichkeiten, Testdaten zu erstellen.

Benutzerdefinierte Daten verwenden

firebase-functions-test bietet eine Reihe von Funktionen zum Erstellen von Daten, die zum Testen Ihrer Funktionen erforderlich sind. Verwenden Sie beispielsweise test.firestore.makeDocumentSnapshot, um eine Firestore-DocumentSnapshot zu erstellen. Das erste Argument sind die Daten, das zweite der vollständige Referenzpfad. Es gibt ein optionales drittes Argument für andere Attribute 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 Zustand davor und einen für den Zustand danach. 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);

In der API-Referenz finden Sie ähnliche Funktionen für alle anderen Datentypen.

Beispieldaten verwenden

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

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

Verwendung von Stub-Daten (für den Offlinemodus)

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

Angenommen, Sie schreiben einen Unittest 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 Mock-Funktionen von Jest, um die Methoden zu stubben.

// The following lines create 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,
    }
  }
};

Assertions erstellen

Nachdem Sie das SDK initialisiert, die Funktionen verpackt und Daten erstellt haben, können Sie die verpackten Funktionen mit den erstellten Daten aufrufen und Assertions zum Verhalten erstellen. Sie können die expect von Jest verwenden, um diese Assertions zu erstellen.

Assertions im Onlinemodus erstellen

Wenn Sie das Firebase Test SDK für Cloud Functions im Onlinemodus initialisiert haben, können Sie mit dem firebase-admin SDK prüfen, ob die gewünschten Aktionen (z. B. ein Schreibvorgang in der Datenbank) ausgeführt wurden.

Im folgenden Beispiel wird bestätigt, 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.
  return admin.database().ref('messages/11111/uppercase').once('value').then((createdSnap) => {
    // Assert that the value is the uppercased version of our input.
    expect(createdSnap.val()).toBe('INPUT');
  });
});

Behauptungen im Offlinemodus aufstellen

Sie können Assertions zum erwarteten Rückgabewert der Funktion erstellen:

const childParam = 'uppercase';
const setParam = 'INPUT';
// Spies/mocks 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 setStub = jest.fn().mockImplementation((val) => val === setParam ? true : undefined);
const childStub = jest.fn().mockImplementation((path) => path === childParam ? { set: setStub } : undefined);
// The following lines create 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,
    }
  }
};
// Wrap the makeUppercase function.
const wrapped = test.wrap(myFunctions.makeUppercase);
// Since we've mocked 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 => {
  expect(makeUppercaseResult).toBe(true);
});

Sie können auch Jest-Mock-Funktionen verwenden, um zu prüfen, ob bestimmte Methoden aufgerufen wurden und ob die erwarteten Parameter verwendet wurden.

HTTP-Funktionen testen

Verwenden Sie zum Testen von HTTP-onCall-Funktionen denselben Ansatz wie zum 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 ein echtes Firebase-Projekt und die zugehörigen Anmeldedaten für Ihre Tests verwenden.

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

  • Stub admin.database(), da addMessage() Daten an die Realtime Database sendet. Im Offlinemodus haben Sie initializeApp() gestubbt, sodass es keine Firebase-App gibt, die das Admin SDK verwenden kann. Die echte admin.database() schlägt mit The default Firebase app does not exist fehl:

    const pushStub = jest.fn().mockResolvedValue({ ref: 'new_ref' });
    const refStub = jest.fn().mockReturnValue({ push: pushStub });
    Object.defineProperty(admin, 'database', {
      configurable: true,
      writable: true,
      value: jest.fn().mockReturnValue({ ref: refStub }),
    });
    
  • Überschreiben Sie die Weiterleitungsfunktion im Antwortobjekt, da sie von addMessage() aufgerufen wird.

  • Verwenden Sie in der Weiterleitungsfunktion expect von Jest, um Aussagen darüber zu treffen, mit welchen Parametern die Weiterleitungsfunktion 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) => {
    expect(code).toBe(303);
    expect(url).toBe('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);

Testbereinigung

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

test.cleanup();

Vollständige Beispiele ansehen und weitere Informationen erhalten

Die vollständigen Beispiele finden Sie im Firebase-GitHub-Repository.

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