Testowanie jednostkowe funkcji w Cloud Functions

Na tej stronie opisujemy sprawdzone metody i narzędzia do tworzenia testów jednostkowych dla funkcji, np. testy, które będą częścią systemu ciągłej integracji (CI). Aby ułatwić testowanie, Firebase udostępnia pakiet SDK Firebase Test dla Cloud Functions. Jest rozpowszechniany na npm jako firebase-functions-test i jest towarzyszącym testowym pakietem SDK firebase-functions. Pakiet SDK Firebase Test SDK dla Cloud Functions:

  • Zapewnia odpowiednią konfigurację i sekcję testów, np. ustawianie i usuwanie zmiennych środowiskowych wymaganych do działania firebase-functions.
  • Generuje przykładowe dane i kontekst zdarzenia, więc musisz tylko określić tylko te pola, które są istotne dla Twojego testu.

Konfiguracja testowa

Zainstaluj platformę testową firebase-functions-test i Mocha, 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, a następnie utwórz w nim nowy plik na potrzeby kodu testowego i nadaj mu nazwę podobną do index.test.js.

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

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

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

Inicjowanie pakietu SDK Firebase Test dla Cloud Functions

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

  1. Tryb online (zalecany): pisz testy, które współdziałają z projektem Firebase przeznaczonym do testowania, aby wykonały działania zapisu w bazie danych, tworzone przez użytkownika itp., a kod testowy mógł sprawdzać wyniki. Oznacza to, że inne pakiety SDK Google używane w Twoich funkcjach również będą działać.
  2. Tryb offline: możesz tworzyć silosowane i offline testy jednostkowe bez efektów ubocznych. Oznacza to, że wszystkie wywołania metod, które wchodzą w interakcje z usługą Firebase (np. zapisywanie w bazie danych lub tworzenie użytkownika), muszą zostać skrócone. W przypadku funkcji Cloud Firestore lub Bazy danych czasu rzeczywistego korzystanie z trybu offline zazwyczaj nie jest zalecane, ponieważ znacznie zwiększa złożoność kodu testowego.

Zainicjuj pakiet SDK w trybie online (zalecane)

Jeśli chcesz pisać testy współdziałające z projektem testowym, musisz podać wartości konfiguracyjne projektu potrzebne do zainicjowania aplikacji za pomocą narzędzia 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 dla aplikacji Apple i Androida.

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

Aby utworzyć plik klucza:

  1. Otwórz okienko Konta 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, 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');

Zainicjuj pakiet SDK w trybie offline

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

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

Pozorowanie wartości konfiguracji

Jeśli używasz functions.config() w kodzie funkcji, możesz symulować 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 symulować wartość w pliku testowym w ten sposób:

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

Importowanie funkcji

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

// 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(), przed zaimportowaniem funkcji musisz go skrócić:

// 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. Zapakuj funkcję, którą chcesz przetestować za pomocą metody test.wrap
  2. Tworzenie danych testowych
  3. Wywołaj funkcję opakowaną, używając utworzonych przez siebie danych testowych i dowolnych pól kontekstu zdarzenia, które chcesz określić.
  4. Wyrażaj sobie spostrzeżenia na temat zachowań.

Najpierw zapakuj funkcję, którą chcesz przetestować. Załóżmy, że w functions/index.js masz funkcję o nazwie makeUppercase, którą chcesz przetestować. Napisz w języku functions/test/index.test.js ten tekst

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

  1. data (wymagane): dane do wysłania do usługi 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 danych.
  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 przy wywołaniu funkcji wrapped nie podasz parametru eventContextOptions, kontekst zdarzenia zostanie wygenerowany i wykorzystaniem rozsądnych pól. 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ąpiono, 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 do wywołania funkcji bazowej. Dane testowe można tworzyć na kilka sposobów.

Korzystanie z danych niestandardowych

firebase-functions-test ma wiele funkcji do tworzenia danych potrzebnych do testowania funkcji. Aby na przykład utworzyć instancję DocumentSnapshot Firestore, użyj polecenia test.firestore.makeDocumentSnapshot. Pierwszy argument to dane, a drugi to pełna ścieżka odniesienia. Występuje też opcjonalny trzeci argument dla innych 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: jeden dla stanu przed i drugi dla stanu po. Następnie możesz za pomocą metody makeChange 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);

Zapoznaj się z dokumentacją interfejsu API, aby uzyskać informacje o podobnych funkcjach w przypadku wszystkich pozostałych typów danych.

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 poszczególnych typów funkcji.

// For Firestore onCreate or onDelete functions
const snap = test.firestore.exampleDocumentSnapshot();
// For Firestore onUpdate or onWrite functions
const change = test.firestore.exampleDocumentSnapshotChange();

W dokumentacji interfejsu API znajdziesz metody pobierania przykładowych danych dla poszczególnych typów funkcji.

Używanie skróconych danych (w trybie offline)

Jeśli pakiet SDK został zainicjowany w trybie offline i testujesz funkcję Cloud Firestore lub Bazy danych czasu rzeczywistego, zamiast tworzyć rzeczywisty obiekt DocumentSnapshot lub DataSnapshot, użyj zwykłego obiektu z atramentami.

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

W obrębie funkcji pole snap jest używane dwukrotnie:

  • snap.val()
  • snap.ref.parent.child('uppercase').set(uppercase)

Utwórz w kodzie testowym zwykły obiekt, w którym będą działać obie te ścieżki kodu, i użyj kodu Sinon, aby utworzyć atrapę 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);

Składanie asercji

Po zainicjowaniu pakietu SDK, pakowaniu funkcji i utworzeniu danych możesz wywoływać opakowane funkcje z utworzonymi danymi i podejmować decyzje dotyczące zachowania. Do formułowania tych założeń możesz użyć biblioteki takiej jak Chai.

Tworzenie asercji w trybie online

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

W przykładzie poniżej widać, że pole „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 asercji w trybie offline

Możesz składać 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żyć narzędzia Sinon szpieg, aby zaświadczyć, że określone metody zostały wywołane z oczekiwanymi parametrami.

Testowanie funkcji HTTP

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

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

  • Używasz functions.config()
  • Twoja funkcja wchodzi w interakcję 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. Oto jak możesz przetestować przykładową funkcję addMessage():

  • Zastąp funkcję przekierowania w obiekcie odpowiedzi, ponieważ funkcja sendMessage() ją wywołuje.
  • W ramach funkcji przekierowania użyj polecenia chai.assert, aby ustalić, jakie parametry ma wywoływać 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 testowe

Na samym końcu kodu testowego wywołaj funkcję czyszczenia. Powoduje to usunięcie zmiennych środowiskowych ustawionych przez pakiet SDK podczas inicjowania i usuwa aplikacje Firebase, które mogły zostać utworzone, jeśli pakiet SDK służy do tworzenia bazy danych DataSnapshot w czasie rzeczywistym lub Firestore DocumentSnapshot.

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 dotyczącej firebase-functions-test.