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. Aby ułatwić testowanie, Firebase udostępnia Firebase Test SDK dla Cloud Functions. Jest on dystrybuowany w npm jako firebase-functions-test i jest towarzyszącym pakietem SDK do testowania pakietu firebase-functions. Firebase Test SDK dla Cloud Functions:

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

Konfiguracja testu

Zainstaluj firebase-functions-test i Mocha, czyli platformę testową, uruchamiając te 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 z kodem testowym 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ć, wpisując npm test w katalogu funkcji.

Inicjowanie Firebase Test SDK dla Cloud Functions

Pakietu firebase-functions-test można używać na 2 sposoby:

  1. 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. rzeczywiście się odbywały, a kod testowy mógł sprawdzać wyniki. Oznacza to również, że inne pakiety SDK Google używane w funkcjach będą działać.
  2. 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 Cloud Firestore lub Realtime Database funkcje, nie zalecamy używania trybu offline , ponieważ znacznie zwiększa to złożoność kodu testowego.

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 Firebase wymagane do zainicjowania aplikacji za pomocą firebase-admin oraz ścieżkę do pliku klucza konta usługi.

Aby uzyskać wartości konfiguracji Firebase:

  1. W konsoli Firebase otwórz stronę ustawień Ustawienia > Ogólne page.

  2. Przewiń do karty Twoje aplikacje i wybierz odpowiednią aplikację.

  3. Pobierz konfigurację Firebase:

    • W przypadku aplikacji na Apple i Androida wybierz opcję pobrania pliku konfiguracyjnego.

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

Aby utworzyć plik klucza:

  1. W konsoli Google Cloud otwórz panel Konta usługi.

  2. Wybierz domyślne konto usługi App Engine i w menu opcji po prawej stronie kliknij Utwórz klucz.

  3. Gdy pojawi się prośba, wybierz typ klucza JSON 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;

Wartość możesz symulować 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 symulowaniu 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 masz admin.initializeApp(), musisz go zastąpić 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. Utwórz dane testowe.
  3. Wywołaj owiniętą funkcję za pomocą utworzonych danych testowych i dowolnych pól kontekstu zdarzenia, które chcesz określić.
  4. Sprawdź zachowanie.

Najpierw owiń funkcję, którą chcesz przetestować. Załóżmy, że w pliku functions/index.js masz funkcję o nazwie makeUppercase, którą chcesz przetestować. Wpisz w pliku functions/test/index.test.js ten kod:

// "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 po wywołaniu. wrapped przyjmuje 2 parametry:

  1. data (wymagany): 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 danych niestandardowych lub przykładowych.
  2. eventContextOptions (opcjonalny): pola kontekstu zdarzenia, które chcesz określić. Kontekst zdarzenia to drugi parametr wysłany do napisanego przez Ciebie modułu obsługi funkcji. Jeśli podczas wywoływania wrapped nie uwzględnisz parametru eventContextOptions, kontekst zdarzenia 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 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

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

Używanie 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, a trzeci (opcjonalny) to inne właściwości migawki, które możesz 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ć 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 API.

Używanie 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 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 z zastępczymi metodami.

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

Sprawdzanie

Po zainicjowaniu pakietu SDK, owinięciu funkcji i utworzeniu danych możesz wywołać owinięte funkcje za pomocą utworzonych danych i sprawdzić zachowanie. Do sprawdzania możesz użyć biblioteki takiej jak Chai do wykonywania tych asercji.

Sprawdzanie w trybie online

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

Poniższy przykład sprawdza, czy w bazie danych projektu testowego zapisano wartość „INPUT”.

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

Sprawdzanie w trybie offline

Możesz sprawdzić oczekiwaną wartość zwracaną 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żyć szpiegów Sinon, aby sprawdzić, czy wywołano określone metody i czy użyto oczekiwanych parametrów.

Testowanie funkcji HTTP

Aby testować funkcje HTTP onCall, użyj tej samej metody co w przypadku testowania funkcji w tle.

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

  • używasz functions.config()
  • funkcja wchodzi w interakcję z projektem w Firebase lub innymi interfejsami API Google i chcesz używać prawdziwego projektu w Firebase oraz jego danych logowania do testów.

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ą funkcja 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 po testach

Na samym końcu kodu testowego wywołaj funkcję czyszczenia. Usuwa ona 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 obiektu DataSnapshot w bazie danych czasu rzeczywistego lub obiektu DocumentSnapshot w Firestore.

test.cleanup();

Przeglądanie pełnych przykładów i dowiedz się więcej

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

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