Testowanie jednostkowe funkcji w Cloud Functions

Na tej stronie opisujemy sprawdzone metody i narzędzia do pisania testów jednostkowych funkcji, np. testów, które byłyby częścią systemu ciągłej integracji (CI). Aby ułatwić testowanie, Firebase udostępnia Firebase Test SDK dla Cloud Functions. Jest on rozpowszechniany w npm jako firebase-functions-test i jest pomocniczym pakietem SDK do testowania firebase-functions. Firebase Test SDK za Cloud Functions:

  • Zapewnia odpowiednią konfigurację i czyszczenie po testach, np. ustawianie i usuwanie zmiennych środowiskowych wymaganych przez firebase-functions.
  • Generuje przykładowe dane i kontekst zdarzenia, dzięki czemu musisz tylko określić pola istotne dla testu.

Konfiguracja testu

Zainstaluj zarówno firebase-functions-test, jak i Mocha, czyli platformę testową, uruchamiając w folderze funkcji te polecenia:

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

Następnie w folderze funkcji utwórz folder test, a w nim nowy plik z kodem testowym. Nadaj mu nazwę, np. index.test.js.

Na koniec zmodyfikuj functions/package.json, aby dodać te elementy:

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

Po napisaniu testów możesz je uruchomić, wpisując npm test w katalogu funkcji.

Inicjuję Firebase Test SDK dla Cloud Functions

Właściwość firebase-functions-test można określić na 2 sposoby:

  1. Tryb online (zalecany): pisz testy, które wchodzą w interakcję z projektem Firebase przeznaczonym do testowania, aby zapisy w bazie danych, tworzenie użytkowników itp. rzeczywiście miały miejsce, a kod testowy mógł sprawdzać wyniki. Oznacza to również, że inne pakiety SDK Google używane w Twoich funkcjach będą działać prawidłowo.
  2. Tryb offline: pisz odseparowane testy jednostkowe offline bez efektów ubocznych. Oznacza to, że wszystkie wywołania metod, które wchodzą w interakcję z usługą Firebase (np. zapisywanie w bazie danych lub tworzenie użytkownika), muszą być zastąpione. Korzystanie z trybu offline nie jest zalecane, jeśli masz funkcje Cloud Firestore lub Realtime Database, ponieważ znacznie zwiększa to złożoność kodu testu.

Inicjowanie pakietu SDK w trybie online (zalecane)

Jeśli chcesz pisać testy, które wchodzą w interakcję z projektem testowym, musisz podać wartości konfiguracji projektu wymagane do zainicjowania aplikacji za pomocą firebase-admin oraz ścieżkę do pliku klucza konta usługi.

Aby uzyskać wartości konfiguracji projektu Firebase:

  1. Otwórz ustawienia projektu w Firebasekonsoli.
  2. W sekcji Twoje aplikacje wybierz odpowiednią aplikację.
  3. W panelu po prawej stronie wybierz opcję pobrania pliku konfiguracyjnego dla aplikacji na urządzenia z Androidem i iOS.

    W przypadku aplikacji internetowych kliknij Konfiguracja, aby wyświetlić wartości konfiguracji.

Aby utworzyć plik klucza:

  1. Otwórz panel Konta usługi w konsoli Google Cloud.
  2. Wybierz App Engine domyślne konto usługi i użyj menu opcji po prawej stronie, aby wybrać Utwórz klucz.
  3. Gdy pojawi się odpowiedni komunikat, wybierz JSON jako typ klucza i kliknij Utwórz.

Po zapisaniu pliku klucza zainicjuj pakiet SDK:

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

Inicjowanie pakietu SDK w trybie offline

Jeśli chcesz pisać testy całkowicie offline, możesz zainicjować pakiet SDK bez żadnych parametrów:

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

Symulowanie wartości konfiguracji

Jeśli w kodzie funkcji używasz functions.config(), możesz symulować wartości konfiguracji. Jeśli na przykład functions/index.js zawiera ten kod:

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

Następnie możesz zasymulować wartość w pliku testowym w ten sposób:

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

Importowanie funkcji

Aby zaimportować funkcje, użyj require, aby zaimportować główny plik funkcji jako moduł. Pamiętaj, aby zrobić to dopiero po zainicjowaniu firebase-functions-test i ustawieniu wartości konfiguracyjnych.

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

Jeśli zainicjowano firebase-functions-testtrybie offline i w kodzie funkcji masz admin.initializeApp(), musisz utworzyć jego atrapę przed zaimportowaniem funkcji:

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

Testowanie funkcji w tle (innych niż HTTP)

Proces testowania funkcji innych niż HTTP obejmuje te kroki:

  1. Owiń funkcję, którą chcesz przetestować, metodą test.wrap.
  2. Tworzenie danych testowych
  3. Wywołaj opakowaną funkcję z utworzonymi danymi testowymi i wszelkimi polami kontekstu zdarzenia, które chcesz określić.
  4. tworzyć asercje dotyczące zachowania,

Najpierw opakuj funkcję, którą chcesz przetestować. Załóżmy, że masz funkcję w functions/index.js o nazwie makeUppercase, którą chcesz przetestować. Napisz poniższy tekst w języku functions/test/index.test.js

// "Wrap" the makeUpperCase function from index.js
const myFunctions = require('../index.js');
const wrapped = test.wrap(myFunctions.makeUppercase);

wrapped to funkcja, która wywołuje funkcję makeUppercase, gdy jest wywoływana. wrapped przyjmuje 2 parametry:

  1. data (wymagane): dane do wysłania do makeUppercase. Odpowiada to bezpośrednio pierwszemu parametrowi wysłanemu do napisanego przez Ciebie modułu obsługi funkcji. firebase-functions-test udostępnia metody tworzenia niestandardowych danych lub danych przykładowych.
  2. eventContextOptions (opcjonalne): pola kontekstu zdarzenia, które chcesz określić. Kontekst zdarzenia to drugi parametr wysyłany do napisanego przez Ciebie modułu obsługi funkcji. Jeśli podczas wywoływania funkcji wrapped nie podasz parametru eventContextOptions, kontekst zdarzenia zostanie wygenerowany z odpowiednimi polami. Niektóre wygenerowane pola możesz zastąpić, podając je tutaj. Pamiętaj, że musisz uwzględnić tylko pola, które chcesz zastąpić. Wszystkie pola, których nie zastąpiono, są generowane.
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
});

Tworzenie danych testowych

Pierwszy parametr opakowanej funkcji to dane testowe, za pomocą których wywoływana jest funkcja bazowa. Dane testowe można tworzyć na wiele sposobów.

Korzystanie z danych niestandardowych

firebase-functions-test ma wiele funkcji do tworzenia danych potrzebnych do testowania funkcji. Na przykład użyj test.firestore.makeDocumentSnapshot, aby utworzyć DocumentSnapshot Firestore. Pierwszy argument to dane, a drugi to pełna ścieżka odniesienia. Istnieje też opcjonalny trzeci argument, który umożliwia określenie innych właściwości migawki.

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

Jeśli testujesz funkcję onUpdate lub onWrite, musisz utworzyć 2 migawki: jedną dla stanu przed i jedną dla stanu po. Następnie możesz użyć metody makeChange, aby utworzyć obiekt Change z tymi migawkami.

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

Podobne funkcje dla wszystkich innych typów danych znajdziesz w dokumentacji interfejsu API.

Korzystanie z przykładowych danych

Jeśli nie musisz dostosowywać danych używanych w testach, interfejs firebase-functions-test udostępnia metody generowania przykładowych danych dla każdego typu funkcji.

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

Metody pobierania przykładowych danych dla każdego typu funkcji znajdziesz w dokumentacji interfejsu API.

Używanie danych zastępczych (w trybie offline)

Jeśli pakiet SDK został zainicjowany w trybie offline i testujesz funkcję Cloud Firestore lub Realtime Database, zamiast tworzyć rzeczywisty obiekt DocumentSnapshot lub DataSnapshot użyj zwykłego obiektu ze stubami.

Załóżmy, że piszesz test jednostkowy dla tej funkcji:

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

W funkcji symbol snap jest używany 2 razy:

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

W kodzie testowym utwórz zwykły obiekt, w którym będą działać obie ścieżki kodu, i użyj biblioteki Sinon, aby utworzyć stuby metod.

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

Tworzenie asercji

Po zainicjowaniu pakietu SDK, opakowaniu funkcji i skonstruowaniu danych możesz wywołać opakowane funkcje za pomocą skonstruowanych danych i sprawdzić, czy działają zgodnie z oczekiwaniami. Do tworzenia tych asercji możesz użyć biblioteki takiej jak Chai.

Tworzenie asercji w trybie online

Jeśli zainicjujesz Firebase Test SDK dla Cloud Functionstrybie online, możesz potwierdzić, że wykonano odpowiednie działania (np. zapis w bazie danych), korzystając z pakietu SDK firebase-admin.

Poniższy przykład potwierdza, że wartość „INPUT” została zapisana w bazie danych projektu testowego.

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

Tworzenie asercji w trybie offline

Możesz tworzyć asercje dotyczące oczekiwanej wartości zwracanej przez funkcję:

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 wrapped(snap).then(makeUppercaseResult => {
  return assert.equal(makeUppercaseResult, true);
});

Możesz też używać szpiegów Sinon, aby sprawdzać, czy wywołano określone metody i czy użyto w nich oczekiwanych parametrów.

Testowanie funkcji HTTP

Aby przetestować funkcje HTTP onCall, użyj tej samej metody co w przypadku testowania funkcji działających w tle.

Jeśli testujesz funkcje HTTP onRequest, użyj firebase-functions-test, gdy:

  • Używasz functions.config()
  • Funkcja wchodzi w interakcję z projektem Firebase lub innymi interfejsami API Google i chcesz używać w testach prawdziwego projektu Firebase i jego danych logowania.

Funkcja HTTP onRequest przyjmuje 2 parametry: obiekt żądania i obiekt odpowiedzi. Oto jak możesz przetestować addMessage()przykładową funkcję:

  • Zastąp funkcję przekierowania w obiekcie odpowiedzi, ponieważ wywołuje ją interfejs sendMessage().
  • W funkcji przekierowania użyj chai.assert, aby sprawdzić, z jakimi parametrami powinna być wywoływana funkcja przekierowania:
// 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);

Czyszczenie testowe

Na samym końcu kodu testowego wywołaj funkcję czyszczenia. Ta funkcja usuwa zmienne środowiskowe ustawione przez pakiet SDK podczas inicjowania oraz usuwa aplikacje Firebase, które mogły zostać utworzone, jeśli pakiet SDK został użyty do utworzenia bazy danych w czasie rzeczywistym DataSnapshot lub Firestore DocumentSnapshot.

test.cleanup();

Zobacz pełne przykłady i dowiedz się więcej

Pełne przykłady znajdziesz w repozytorium Firebase GitHub.

Więcej informacji znajdziesz w dokumentacji interfejsu API dotyczącej firebase-functions-test.