Testowanie jednostkowe funkcji w Cloud Functions

Na tej stronie opisujemy sprawdzone metody i narzędzia do pisania testów jednostkowych funkcji, takich jak testy wchodzące w skład systemu ciągłej integracji (CI). Aby ułatwić testowanie, Firebase udostępnia pakiet SDK Firebase dla Cloud Functions. Jest rozpowszechniany w npm jako firebase-functions-test i jest testowym pakietem SDK towarzyszącym dla firebase-functions. Pakiet SDK Firebase Test dla Cloud Functions:

  • Dba o odpowiednią konfigurację i demontaż testów, w tym na ustawianie i usuwanie zmiennych środowiskowych wymaganych przez firebase-functions.
  • Generuje przykładowe dane i kontekst zdarzenia, dzięki czemu wystarczy określić tylko te pola, które są istotne dla testu.

Testuj konfigurację

Zainstaluj zarówno platformę firebase-functions-test, jak i platformę testową Mocha, 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 mniej więcej index.test.js.

Na koniec zmodyfikuj functions/package.json, by dodać:

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

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

Inicjowanie pakietu SDK do testów Firebase dla Cloud Functions

Z usługi firebase-functions-test można korzystać na 2 sposoby:

  1. Tryb online (zalecany): napisz testy, które wchodzą w interakcję z projektem Firebase przeznaczonym do testowania, tak aby zapisy w bazie danych, tworzone przez użytkowników itp. faktycznie miały miejsce, a Twój kod testowy mógł sprawdzić wyniki. Oznacza to, że będą też działać inne pakiety SDK Google używane w Twoich funkcjach.
  2. Tryb offline: pisz testy silosów i jednostek offline bez efektów ubocznych. Oznacza to, że wszelkie wywołania metod, które wchodzą w interakcję z usługą Firebase (np. zapisywanie w bazie danych lub tworzenie użytkownika), muszą być skrócone. Używanie trybu offline nie jest zalecane w przypadku funkcji Cloud Firestore lub Bazy danych czasu rzeczywistego, ponieważ znacznie zwiększa złożoność kodu testowego.

Zainicjuj pakiet SDK w trybie online (zalecane)

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

Aby pobrać wartości konfiguracyjne 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ę pobrania pliku konfiguracji aplikacji Apple lub na Androida.

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

Aby utworzyć plik klucza:

  1. Otwórz okienko kont usługi w konsoli Google Cloud.
  2. Wybierz domyślne konto usługi App Engine i w menu opcji po prawej stronie wybierz Utwórz klucz.
  3. Gdy pojawi się prośba, jako typ klucza wybierz 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');

Zainicjuj pakiet SDK w trybie offline

Jeśli chcesz przeprowadzać testy w trybie offline, możesz zainicjować pakiet SDK bez żadnych parametrów:

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

Naśladowanie wartości konfiguracyjnych

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

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

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

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

Importowanie funkcji

Aby zaimportować funkcje, użyj narzędzia require do zaimportowania głównego pliku funkcji jako modułu. Pamiętaj, aby zrobić to dopiero po zainicjowaniu elementu firebase-functions-test i zafałszowaniu 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-test w trybie offline, a w kodzie funkcji masz admin.initializeApp(), 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 działających w tle (innych niż HTTP)

Proces testowania funkcji innych niż HTTP składa się z tych etapów:

  1. Opakuj funkcję, którą chcesz przetestować, za pomocą metody test.wrap
  2. Utwórz dane testowe
  3. Wywołaj funkcję opakowaną z utworzonymi danymi testowymi i wszelkimi polami kontekstu zdarzenia, które chcesz określić.
  4. Twierdzenie dotyczące zachowania.

Najpierw zapakuj funkcję, którą chcesz przetestować. Załóżmy, że masz w functions/index.js funkcję o nazwie makeUppercase, którą chcesz przetestować. Napisz 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 makeUppercase, gdy zostanie wywołana. wrapped przyjmuje 2 parametry:

  1. data (wymagany): dane do wysłania do usługi makeUppercase. Odpowiada to bezpośrednio pierwszemu parametrowi wysłanemu do utworzonego przez Ciebie modułu obsługi funkcji. firebase-functions-test udostępnia metody tworzenia danych niestandardowych lub przykładowych danych.
  2. eventContextOptions (opcjonalnie): pola kontekstu zdarzenia, który chcesz określić. Kontekst zdarzenia to drugi parametr wysłany do napisanego przez Ciebie modułu obsługi funkcji. Jeśli nie podasz parametru eventContextOptions przy wywołaniu metody wrapped, kontekst zdarzenia będzie nadal generowany z użyciem zrozumiałych pól. 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óre nie zostały zastąpione, 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 służące do wywołania funkcji bazowej. Dane testowe można tworzyć na wiele sposobów.

Korzystanie z danych niestandardowych

firebase-functions-test zawiera wiele funkcji do tworzenia danych potrzebnych do testowania funkcji. Możesz na przykład użyć test.firestore.makeDocumentSnapshot, aby utworzyć instancję DocumentSnapshot w Firestore. Pierwszym argumentem są dane, drugi argument to pełna ścieżka odniesienia, a opcjonalny trzeci argument określa inne właściwości zrzutu, 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 zrzuty: 1 dla stanu przed i 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);

Informacje o podobnych funkcjach w przypadku pozostałych typów danych znajdziesz w dokumentacji interfejsu API.

Wykorzystanie 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 skróconych danych (na potrzeby trybu offline)

Jeśli pakiet SDK został zainicjowany w trybie offline i testujesz funkcję Cloud Firestore lub bazy danych czasu rzeczywistego, zamiast tworzenia obiektu DocumentSnapshot lub DataSnapshot użyj zwykłego obiektu z narożnikami.

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 obie te ścieżki kodu będą działać, i użyj kodu Sinon, aby zakończyć stosowanie 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);

Przedstawianie asercji

Po zainicjowaniu pakietu SDK, zapakowaniu funkcji i utworzeniu danych możesz wywoływać funkcje opakowane ze skonstruowanymi danymi i zgłaszać asercje dotyczące zachowania. Do tworzenia takich asercji możesz użyć biblioteki takiej jak Chai.

Pisanie asercji w trybie online

Jeśli pakiet SDK Firebase do Cloud Functions został zainicjowany w trybie online, możesz za pomocą pakietu SDK firebase-admin potwierdzić, że odpowiednie działania (na przykład zapis w bazie danych) zostały wykonane.

Przykład poniżej potwierdza, że w bazie danych projektu testowego została zapisana zmienna „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');
  });
});

Wprowadzanie asercji w trybie offline

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

Możesz też używać funkcji Sinon szpieg, aby potwierdzić, że określone metody zostały wywołane i z oczekiwanymi parametrami.

Testowanie funkcji HTTP

Aby przetestować funkcje HTTP onCall, użyj tego samego sposobu co w przypadku testowania funkcji działających w tle.

Jeśli testujesz funkcje HTTP onRequest, użyj właściwości firebase-functions-test, jeśli:

  • Używasz functions.config()
  • Twoja funkcja współdziała z projektem Firebase lub innymi interfejsami API Google i chcesz w testach użyć prawdziwego projektu Firebase oraz jego danych logowania.

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

  • Zastąp funkcję przekierowania w obiekcie odpowiedzi, bo sendMessage() ją wywołuje.
  • W obrębie funkcji przekierowania stosuj chai.assert, aby decydować o tym, jakie parametry ma zostać wywołana przez tę funkcję:
// 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

Wywołaj funkcję czyszczenia na samym końcu kodu testowego. Spowoduje to ustawienie zmiennych środowiskowych ustawionych przez pakiet SDK podczas jego inicjowania oraz usunięcie aplikacji Firebase, które mogły zostać utworzone, jeśli do utworzenia bazy danych DataSnapshot lub Firestore DocumentSnapshot za pomocą pakietu SDK utworzono w czasie rzeczywistym.

test.cleanup();

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

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

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