Na tej stronie opisujemy sprawdzone metody i narzędzia do pisania testów jednostkowych funkcji, takich jak testy, które byłyby częścią systemu trybu 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 pakietem SDK do testowania towarzyszącym pakietowi firebase-functions. Firebase Test SDK dla Cloud Functions:
- Zajmuje się odpowiednią konfiguracją i usuwaniem testów, np. ustawianiem i usuwaniem zmiennych środowiskowych wymaganych przez
firebase-functions. - Generuje przykładowe dane i kontekst zdarzenia, dzięki czemu musisz określić tylko pola, które są istotne dla testu.
Konfiguracja testu
Zainstaluj firebase-functions-test i Jest, platformę testową, uruchamiając te polecenia w folderze funkcji:
npm install --save-dev firebase-functions-test
npm install --save-dev jest
Następnie utwórz folder test w folderze funkcji, a w nim nowy plik na kod testowy i nadaj mu nazwę, np. index.test.js.
Na koniec zmodyfikuj plik functions/package.json, aby dodać te elementy:
"scripts": {
"test": "jest"
}
Po napisaniu testów możesz je uruchomić, wykonując polecenie npm test w katalogu funkcji.
Inicjuję Firebase Test SDK dla: Cloud Functions
Z firebase-functions-test możesz korzystać na 2 sposoby:
- Tryb online (zalecany): pisz testy, które wchodzą w interakcję z projektem w Firebase przeznaczonym do testowania, aby zapisy w bazie danych, tworzenie użytkowników itp. faktycznie miały miejsce, a kod testowy mógł sprawdzać wyniki. Oznacza to, że inne pakiety SDK Google używane w funkcjach również będą działać.
- Tryb offline: pisz odizolowane 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. Jeśli masz funkcje Cloud Firestore lub Realtime Database, nie zalecamy korzystania z trybu offline, ponieważ znacznie zwiększa to złożoność kodu testowego.
Inicjowanie pakietu SDK w trybie online (zalecane)
Jeśli chcesz napisać testy, które będą wchodzić w interakcję z projektem testowym, musisz podać wartości konfiguracji Firebase, 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 Firebase:
W konsoli Firebase otwórz stronę
Ustawienia > Ogólne.Przewiń do karty Twoje aplikacje i wybierz odpowiednią aplikację.
Pobierz konfigurację Firebase:
W przypadku aplikacji na urządzenia Apple i Androida wybierz opcję pobrania pliku konfiguracji.
W przypadku aplikacji internetowych wybierz Konfiguracja, aby wyświetlić wartości konfiguracji.
Aby utworzyć plik klucza:
W konsoli Google Cloud otwórz panel Konta usługi.
Wybierz domyślne konto usługi App Engine i użyj menu opcji po prawej stronie, aby wybrać Utwórz klucz.
Gdy pojawi się prośba, 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 plik 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' }});
Importuję Twoje funkcje
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 utworzeniu próbnych wartości konfiguracji.
// after firebase-functions-test has been initialized
const myFunctions = require('../index.js'); // relative path to functions code
Jeśli pakiet firebase-functions-test został zainicjowany w trybie offline, a w kodzie funkcji znajduje się admin.initializeApp(), musisz go zastąpić przed zaimportowaniem funkcji. W pliku testowym wymagaj firebase-admin jako admin, a następnie zastąp initializeApp() makietą Jest:
// 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');
Testowanie funkcji działających w tle (innych niż HTTP)
Proces testowania funkcji innych niż HTTP obejmuje te kroki:
- Opakuj funkcję, którą chcesz przetestować, metodą
test.wrap. - Tworzenie danych testowych
- Wywołaj funkcję opakowaną, używając utworzonych danych testowych i dowolnych pól kontekstu zdarzenia, które chcesz określić.
- tworzyć asercje dotyczące zachowania,
Najpierw opakuj funkcję, którą chcesz przetestować. Załóżmy, że masz w functions/index.js funkcję o nazwie makeUppercase, którą chcesz przetestować. Napisz to, co poniżej, 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 po wywołaniu wywołuje makeUppercase. wrapped przyjmuje 2 parametry:
- data (wymagany): dane, które mają zostać wysłane do
makeUppercase. Odpowiada on bezpośrednio pierwszemu parametrowi wysłanemu do utworzonego przez Ciebie modułu obsługi funkcji.firebase-functions-testudostępnia metody tworzenia niestandardowych danych lub przykładowych danych. - eventContextOptions (opcjonalnie): 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
wrappednie podasz parametrueventContextOptions, kontekst zdarzenia i tak zostanie wygenerowany z odpowiednimi polami. Możesz zastąpić niektóre wygenerowane pola, określając je tutaj. Pamiętaj, że musisz uwzględnić tylko te pola, które chcesz zastąpić. Wszystkie pola, których nie zastąpisz, zostaną wygenerowane.
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
Pierwszym parametrem funkcji opakowanej są dane testowe, za pomocą których wywoływana jest funkcja bazowa. Dane testowe można tworzyć na kilka sposobów.
Używanie danych niestandardowych
firebase-functions-test zawiera szereg 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 pozwala określić inne właściwości zrzutu.
// 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 zrzuty: jeden dla stanu przed i jeden dla stanu po. Następnie możesz użyć metody makeChange, aby utworzyć obiekt Change z tymi zrzutami.
// 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, 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();
Metody uzyskiwania 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, użyj zwykłego obiektu z metodami zastępczymi zamiast tworzyć rzeczywisty obiekt DocumentSnapshot lub DataSnapshot.
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); });
Wewnątrz funkcji snap jest używana dwukrotnie:
snap.val()snap.ref.parent.child('uppercase').set(uppercase)
W kodzie testowym utwórz zwykły obiekt, w którym obie te ścieżki kodu będą działać, i użyj funkcji imitujących Jest, aby zastąpić metody.
// 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, } } };
Tworzenie asercji
Po zainicjowaniu pakietu SDK, opakowaniu funkcji i skonstruowaniu danych możesz wywołać opakowane funkcje ze skonstruowanymi danymi i wykonać asercje dotyczące zachowania. Do tworzenia tych asercji możesz użyć expect Jesta.
Tworzenie asercji w trybie online
Jeśli zainicjujesz Firebase Test SDK dla Cloud Functions w trybie online, możesz potwierdzić, że żądane działania (np. zapis w bazie danych) zostały wykonane, używając pakietu SDK firebase-admin.
W przykładzie poniżej potwierdzono, ż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. 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'); }); });
Tworzenie asercji w trybie offline
Możesz tworzyć asercje dotyczące oczekiwanej wartości zwracanej przez funkcję:
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); });
Możesz też użyć funkcji imitujących Jest, aby sprawdzić, czy określone metody zostały wywołane i czy użyto 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 współdziała z projektem w Firebase lub innymi interfejsami API Google i chcesz użyć w testach prawdziwego projektu w Firebase i jego danych logowania.
Funkcja HTTP onRequest przyjmuje 2 parametry: obiekt żądania i obiekt odpowiedzi. Oto jak możesz przetestować przykładową funkcję addMessage():
Zastąp
admin.database(), ponieważaddMessage()przesyła dane do bazy danych czasu rzeczywistego. W trybie offline metodainitializeApp()została zastąpiona, więc nie ma aplikacji w Firebase, której mógłby użyć pakiet Admin SDK, a prawdziwa metodaadmin.database()kończy się niepowodzeniem z błędemThe default Firebase app does not exist: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 }), });Zastąp funkcję przekierowania w obiekcie odpowiedzi, ponieważ wywołuje ją
addMessage().W funkcji przekierowania użyj metody
expectplatformy Jest, aby utworzyć asercje dotyczące parametrów, z którymi 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) => { 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);
Czyszczenie testowe
Na samym końcu kodu testowego wywołaj funkcję czyszczenia. Spowoduje to usunięcie zmiennych środowiskowych ustawionych przez pakiet SDK podczas inicjowania oraz aplikacji w Firebase, które mogły zostać utworzone, jeśli pakiet SDK został użyty do utworzenia bazy danych czasu rzeczywistego DataSnapshot lub Firestore DocumentSnapshot.
test.cleanup();
Przejrzyj kompletne przykłady i dowiedz się więcej
Kompletne przykłady znajdziesz w repozytorium Firebase na GitHubie.
- Testowanie funkcji Realtime Database i HTTP w trybie online
- Testowanie funkcji Realtime Database i HTTP w trybie offline
Więcej informacji znajdziesz w dokumentacji interfejsu API firebase-functions-test.