Testowanie jednostkowe funkcji w Cloud Functions

Na tej stronie opisano 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 stanowi towarzyszący pakiet SDK do testów dla pakietu firebase-functions. Firebase Test SDK dla Cloud Functions:

  • Zajmuje się odpowiednią konfiguracją i demontażem testów, np. ustawianiem i usuwaniem zmiennych środowiskowych potrzebnych 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 testowa

Zainstaluj firebase-functions-test i Mocha, framework testowy, uruchamiając te polecenia w folderze funkcji:

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

Następnie w folderze functions utwórz folder test, 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": "mocha --reporter spec"
}

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

Inicjuję Firebase Test SDK dla Cloud Functions

Z elementu firebase-functions-test możesz korzystać na 2 sposoby:

  1. Tryb online (zalecany): napisz testy, które będą wchodzić 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ł sprawdzić wyniki. Oznacza to też, że inne pakiety Google SDK używane w Twoich funkcjach również będą działać.
  2. Tryb offline: pisanie testów jednostkowych w trybie offline bez efektów ubocznych. Oznacza to, że wszystkie wywołania metod, które współpracują z usługą Firebase (np. zapisywanie w bazie danych lub tworzenie użytkownika), muszą być zastąpione przez stuby. 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 testowego.

Inicjowanie pakietu SDK w trybie online (zalecane)

Jeśli chcesz pisać testy, które będą wchodzić w interakcję z projektem testowym, musisz podać wartości konfiguracji projektu, które są potrzebne do zainicjowania aplikacji za pomocą funkcji 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 sekcji Twoje aplikacje wybierz odpowiednią aplikację.
  3. W panelu po prawej stronie wybierz opcję pobierania pliku konfiguracyjnego dla aplikacji na urządzenia z systemem Android i Apple.

    W przypadku aplikacji internetowych wybierz 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, a następnie w menu opcji po prawej stronie wybierz Utwórz klucz.
  3. Gdy pojawi się odpowiedni komunikat, wybierz typ klucza JSON i kliknij Utwórz.

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

Inicjowanie pakietu SDK w trybie offline

Jeśli chcesz napisać 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')();

Symulowanie wartości konfiguracji

Jeśli w kodzie funkcji używasz funkcji functions.config(), możesz zasymulować wartości konfiguracji. Jeśli np. 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 funkcji require, aby zaimportować plik główny funkcji jako moduł. Zrób to dopiero po zainicjowaniu firebase-functions-test i ustawieniu wartości konfiguracji.

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

Jeśli zainicjowałeś funkcję firebase-functions-test w trybie offline, a w kodzie funkcji masz funkcję admin.initializeApp(), musisz utworzyć jej szablon 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 działających w tle (nie HTTP)

Proces testowania funkcji innych niż HTTP obejmuje te kroki:

  1. Otocz funkcję, którą chcesz przetestować, metodą test.wrap
  2. Tworzenie danych testowych
  3. Wywołaj opakowaną funkcję z utworzonymi przez siebie danymi testowymi i dowolnymi polami kontekstu zdarzenia, które chcesz określić.
  4. robić założenia dotyczące zachowania;

Najpierw owiń funkcję, którą chcesz przetestować. Załóżmy, że masz w functions/index.js funkcję o nazwie makeUppercase, którą chcesz przetestować. Napisać 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. wrappedprzyjmuje 2 parametry:

  1. data (wymagane): dane do wysłania do makeUppercase. Jest on bezpośrednio powiązany z pierwszym parametrem wysłanym do napisanego przez Ciebie modułu funkcji. firebase-functions-test udostępnia metody tworzenia danych niestandardowych lub przykładowych.
  2. 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 nie podasz parametru eventContextOptions podczas wywoływania funkcji wrapped, nadal będzie generowany kontekst zdarzenia z odpowiednimi polami. Możesz zastąpić niektóre wygenerowane pola, 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 funkcji opakowanej to dane testowe, z którymi wywoływana jest funkcja podstawowa. Dane testowe można tworzyć na kilka sposobów.

Korzystanie z danych niestandardowych

firebase-functions-test zawiera wiele funkcji do tworzenia danych potrzebnych do testowania funkcji. Na przykład możesz użyć test.firestore.makeDocumentSnapshot, aby utworzyć w Firestore kolekcję DocumentSnapshot. Pierwszy argument to dane, a drugi to pełna ścieżka referencyjna. Opcjonalny trzeci argument służy do określania innych właściwości zrzutu ekranu.

// 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: jeden dla stanu sprzed i jeden 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);

Więcej informacji o podobnych funkcjach 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 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 uzyskiwania przykładowych danych dla każdego typu funkcji znajdziesz w dokumentacji interfejsu API.

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

Jeśli inicjujesz pakiet SDK w trybie offline i testujesz funkcję Cloud Firestore lub Realtime Database, użyj zwykłego obiektu z zastępnikami zamiast tworzyć rzeczywiste obiekty 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ż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 ścieżki kodu, a do testowania metod użyj Sinon.

// 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 stwierdzeń

Po zainicjowaniu pakietu SDK, owinięciu funkcji i utworzeniu danych możesz wywoływać owinięte funkcje z utworzonymi danymi i wyraźnie określać ich działanie. Do tworzenia tych stwierdzeń możesz użyć biblioteki takiej jak Chai.

Tworzenie stwierdzeń w trybie online

Jeśli Firebase Test SDK dla Cloud Functions został zainicjowany w trybie online, możesz za pomocą pakietu SDK firebase-admin sprawdzić, czy zostały wykonane żądane działania (np. zapisanie danych w bazie danych).

Przykład poniżej 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');
  });
});

Tworzenie stwierdzeń w trybie offline

Możesz określić oczekiwaną wartość zwracaną funkcji:

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 też używać podsłuchiwania kodu Sinon, aby sprawdzić, czy zostały wywołane określone metody i czy mają one oczekiwane parametry.

Testowanie funkcji HTTP

Aby przetestować funkcje onCall HTTP, zastosuj takie samo podejście jak w przypadku testowania funkcji uruchamianych w tle.

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

  • Używasz functions.config()
  • Twoja funkcja wchodzi w interakcję z projektem Firebase lub innymi interfejsami API Google, a Ty chcesz używać do testów rzeczywistego projektu 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 funkcję przekierowania w obiekcie odpowiedzi, ponieważ sendMessage()wywołuje ją.
  • W funkcji przekierowania użyj funkcji chai.assert, aby sformułować twierdzenia dotyczące parametrów, z którymi należy wywoływać funkcję 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);

Testowanie porządkowania

Na samym końcu kodu testowego wywołaj funkcję czyszczenia. Spowoduje to zresetowanie zmiennych środowiskowych ustawionych przez pakiet SDK podczas inicjalizacji oraz usunięcie aplikacji Firebase, które mogły zostać utworzone, jeśli za pomocą pakietu SDK utworzysz bazę danych w czasie rzeczywistym DataSnapshot lub Firestore DocumentSnapshot.

test.cleanup();

Zapoznaj się z pełnymi przykładami i dowiedz się więcej

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

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