Catch up on highlights from Firebase at Google I/O 2023. Learn more

Testowanie jednostkowe funkcji chmury

Ta strona opisuje najlepsze praktyki i narzędzia do pisania testów jednostkowych dla twoich funkcji, takich jak testy, które byłyby częścią systemu ciągłej integracji (CI). Aby ułatwić testowanie, Firebase udostępnia pakiet Firebase Test SDK for Cloud Functions. Jest dystrybuowany na npm jako firebase-functions-test i jest towarzyszącym zestawem testowym SDK do firebase-functions . Testowy pakiet SDK Firebase dla funkcji w chmurze:

  • Zajmuje się odpowiednią konfiguracją i demontażem testów, takich jak ustawianie i usuwanie zmiennych środowiskowych potrzebnych przez firebase-functions .
  • Generuje przykładowe dane i kontekst zdarzenia, dzięki czemu wystarczy określić tylko te pola, które są istotne dla twojego testu.

Konfiguracja testowa

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

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

Następnie utwórz folder test w folderze funkcji, utwórz w nim nowy plik dla kodu testowego i nazwij go na przykład index.test.js .

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

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

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

Inicjuję testowy pakiet SDK Firebase dla Cloud Functions

Istnieją dwa sposoby użycia firebase-functions-test :

  1. Tryb online (zalecany): pisz testy, które wchodzą w interakcję z projektem Firebase przeznaczonym do testowania, tak aby zapisywanie w bazie danych, tworzenie przez użytkowników itp. odbywało się faktycznie, a kod testowy mógł sprawdzać wyniki. Oznacza to również, że inne pakiety Google SDK używane w Twoich funkcjach również będą działać.
  2. Tryb offline: pisz wyciszone i offline testy jednostkowe bez skutków ubocznych. Oznacza to, że wszelkie wywołania metod, które wchodzą w interakcję z produktem Firebase (np. zapisywanie do bazy danych lub tworzenie użytkownika), muszą zostać skrócone. Korzystanie z trybu offline zasadniczo nie jest zalecane, jeśli masz funkcje Cloud Firestore lub Realtime Database, ponieważ znacznie zwiększa to złożoność kodu testowego.

Zainicjuj SDK w trybie online (zalecane)

Jeśli chcesz napisać testy, które współdziałają z projektem testowym, musisz podać wartości konfiguracji projektu, które są potrzebne 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 konsoli Firebase .
  2. W Twoich aplikacjach wybierz żądaną aplikację.
  3. W prawym okienku wybierz opcję pobrania pliku konfiguracyjnego dla aplikacji Apple i Android.

    W przypadku aplikacji internetowych wybierz opcję Konfiguruj , aby wyświetlić wartości konfiguracyjne.

Aby utworzyć plik klucza:

  1. Otwórz panel Konta usług w Google Cloud Console.
  2. Wybierz domyślne konto usługi App Engine i użyj menu opcji po prawej stronie, aby wybrać opcję Utwórz klucz .
  3. Po wyświetleniu monitu wybierz JSON jako typ klucza i kliknij Utwórz .

Po zapisaniu pliku klucza zainicjuj 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');

Zainicjuj SDK w trybie offline

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

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

Wyśmiewanie wartości konfiguracyjnych

Jeśli używasz functions.config() w swoim kodzie funkcji, możesz kpić z wartości konfiguracji. Na przykład, jeśli functions/index.js zawiera następujący kod:

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

Następnie możesz kpić z wartości w pliku testowym w następujący sposób:

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

Importowanie twoich funkcji

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

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

Jeśli zainicjowałeś firebase-functions-test w trybie offline i masz admin.initializeApp() w swoim kodzie funkcji, musisz go skrócić 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 tła (innych niż HTTP).

Proces testowania funkcji innych niż HTTP obejmuje następujące kroki:

  1. Zawiń funkcję, którą chcesz przetestować, za pomocą metody test.wrap
  2. Skonstruuj dane testowe
  3. Wywołaj opakowaną funkcję z utworzonymi danymi testowymi i dowolnymi polami kontekstu zdarzenia, które chcesz określić.
  4. Twierdź o zachowaniu.

Najpierw zawiń funkcję, którą chcesz przetestować. Załóżmy, że masz funkcję w functions/index.js o nazwie makeUppercase , którą chcesz przetestować. Napisz następujące informacje w 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 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 procedury obsługi funkcji, którą napisałeś. firebase-functions-test zapewnia metody konstruowania danych niestandardowych lub danych przykładowych.
  2. eventContextOptions (opcjonalnie): pola kontekstu zdarzenia, który chcesz określić. Kontekst zdarzenia to drugi parametr wysłany do procedury obsługi funkcji, którą napisałeś. Jeśli nie podasz parametru eventContextOptions podczas wywoływania metody wrapped , nadal generowany jest kontekst zdarzenia z sensownymi polami. Możesz zastąpić niektóre z wygenerowanych pól, określając je tutaj. Pamiętaj, że musisz uwzględnić tylko te pola, które chcesz zastąpić. Zostaną wygenerowane wszystkie pola, których nie zastąpiłeś.
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
});

Konstruowanie danych testowych

Pierwszym parametrem opakowanej funkcji są dane testowe, z którymi należy wywołać funkcję bazową. Istnieje wiele sposobów konstruowania danych testowych.

Korzystanie z danych niestandardowych

firebase-functions-test ma wiele funkcji do konstruowania danych potrzebnych do testowania twoich funkcji. Na przykład użyj test.firestore.makeDocumentSnapshot do utworzenia DocumentSnapshot Firestore . Pierwszym argumentem są dane, a drugim pełna ścieżka referencyjna. Opcjonalny trzeci argument określa inne właściwości migawki, które można określić.

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

Zapoznaj się z dokumentacją interfejsu API, aby zapoznać się z podobnymi funkcjami dla wszystkich innych typów danych.

Korzystając z przykładowych danych

Jeśli nie musisz dostosowywać danych używanych w testach, to firebase-functions-test oferuje 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();

Zapoznaj się z dokumentacją interfejsu API , aby zapoznać się z metodami uzyskiwania przykładowych danych dla każdego typu funkcji.

Korzystanie ze skróconych danych (w trybie offline)

Jeśli zestaw SDK został zainicjowany w trybie offline i testujesz funkcję Cloud Firestore lub Realtime Database, powinieneś użyć zwykłego obiektu z kodami pośredniczącymi, zamiast tworzyć rzeczywisty DocumentSnapshot lub DataSnapshot .

Załóżmy, że piszesz test jednostkowy dla następującej 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);
    });

Wewnątrz funkcji snap jest używany dwukrotnie:

  • 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 te ścieżki kodu, i użyj Sinon do zastąpienia 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);

Dokonywanie twierdzeń

Po zainicjowaniu zestawu SDK, opakowaniu funkcji i skonstruowaniu danych można wywołać opakowane funkcje ze skonstruowanymi danymi i dokonać asercji dotyczących zachowania. Możesz użyć biblioteki, takiej jak Chai, aby dokonać tych twierdzeń.

Dokonywanie twierdzeń w trybie online

Jeśli zestaw Firebase Test SDK for Cloud Functions został zainicjowany w trybie online , możesz potwierdzić, że wymagane działania (takie jak zapis bazy danych) zostały wykonane przy użyciu pakietu SDK firebase-admin .

Poniższy przykład potwierdza, że ​​„INPUT” zostało zapisane 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');
  });
});

Dokonywanie asercji w trybie offline

Możesz dokonać asercji dotyczących 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 assert.equal(wrapped(snap), true);

Możesz także użyć szpiegów Sinon , aby potwierdzić, że zostały wywołane określone metody iz oczekiwanymi parametrami.

Testowanie funkcji HTTP

Aby przetestować funkcje HTTP onCall, użyj tego samego podejścia, co przy testowaniu funkcji działających w tle .

Jeśli testujesz funkcje HTTP onRequest, powinieneś użyć firebase-functions-test jeśli:

  • Używasz functions.config()
  • Twoja funkcja współdziała z projektem Firebase lub innymi interfejsami API Google i chcesz użyć prawdziwego projektu Firebase i jego danych uwierzytelniających do swoich testów.

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

  • Zastąp funkcję przekierowania w obiekcie odpowiedzi, ponieważ sendMessage() ją wywołuje.
  • W ramach funkcji przekierowującej użyj chai.assert , aby pomóc w ustaleniu, z jakimi parametrami należy wywołać funkcję przekierowującą:
// 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);

Oczyszczanie testu

Na samym końcu kodu testowego wywołaj funkcję czyszczenia. Spowoduje to anulowanie ustawień zmiennych środowiskowych ustawionych przez pakiet SDK podczas jego inicjalizacji i usunięcie aplikacji Firebase, które mogły zostać utworzone, jeśli użyto pakietu SDK do utworzenia DataSnapshot bazy danych w czasie rzeczywistym lub Firestore DocumentSnapshot .

test.cleanup();

Przejrzyj kompletne przykłady i dowiedz się więcej

Możesz przejrzeć pełne przykłady w repozytorium GitHub Firebase.

Aby dowiedzieć się więcej, zapoznaj się z dokumentacją interfejsu API dla firebase-functions-test .