Tests unitaires de Cloud Functions

Cette page décrit les bonnes pratiques et les outils permettant d'écrire des tests unitaires pour vos fonctions, tels que les tests qui feraient partie d'un système d'intégration continue (CI). Pour faciliter les tests, Firebase fournit le Firebase Test SDK pour Cloud Functions. Il est distribué sur npm sous le nom firebase-functions-test et est un SDK de test complémentaire à firebase-functions. Le Firebase Test SDK pour Cloud Functions :

  • Gère la configuration et la suppression appropriées de vos tests, par exemple en définissant et en supprimant les variables d'environnement requises par firebase-functions.
  • Génère des exemples de données et de contexte d'événement, de sorte que vous n'avez qu'à spécifier les champs pertinents pour votre test.

Configuration des tests

Installez firebase-functions-test et Mocha, un framework de test, en exécutant les commandes suivantes dans votre dossier de fonctions :

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

Créez ensuite un dossier test dans le dossier des fonctions, créez-y un nouveau fichier pour votre code de test et nommez-le, par exemple, index.test.js.

Enfin, modifiez functions/package.json pour ajouter les éléments suivants :

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

Une fois les tests écrits, vous pouvez les exécuter en exécutant npm test dans votre répertoire de fonctions.

Initialisation de Firebase Test SDK pour Cloud Functions

Vous pouvez utiliser firebase-functions-test de deux manières :

  1. Mode en ligne (recommandé) : écrivez des tests qui interagissent avec un projet Firebase dédié aux tests afin que les écritures dans la base de données, les créations d'utilisateurs, etc. se produisent réellement et que votre code de test puisse inspecter les résultats. Cela signifie également que les autres SDK Google utilisés dans vos fonctions fonctionneront également.
  2. Mode hors connexion : écrivez des tests unitaires isolés et hors connexion sans effets secondaires. Cela signifie que tous les appels de méthode qui interagissent avec un produit Firebase (par exemple, l'écriture dans la base de données ou la création d'un utilisateur) doivent être simulés. L'utilisation du mode hors connexion n'est généralement pas recommandée si vous disposez de Cloud Firestore ou Realtime Database fonctions, car cela augmente considérablement la complexité de votre code de test.

Initialiser le SDK en mode en ligne (recommandé)

Si vous souhaitez écrire des tests qui interagissent avec un projet de test, vous devez fournir les valeurs de configuration Firebase nécessaires à l'initialisation de l'application via firebase-admin, ainsi que le chemin d'accès à un fichier de clé de compte de service.

Pour obtenir vos valeurs de configuration Firebase :

  1. Dans la console Firebase, accédez à la page Settings > General (Paramètres > Général).

  2. Faites défiler la page jusqu'à la fiche Your apps (Vos applications), puis sélectionnez l'application souhaitée.

  3. Obtenez votre configuration Firebase :

    • Pour les applications Apple et Android, sélectionnez l'option permettant de télécharger un fichier de configuration.

    • Pour les applications Web, sélectionnez Config pour afficher les valeurs de configuration.

Pour créer un fichier de clé :

  1. Dans la console Google Cloud, accédez au volet Service Accounts.

  2. Sélectionnez le compte de service par défaut App Engine, puis utilisez le menu d'options à droite pour sélectionner Create key (Créer une clé).

  3. Lorsque vous y êtes invité, sélectionnez JSON pour le type de clé, puis cliquez sur Create (Créer).

Après avoir enregistré le fichier de clé, initialisez le 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');

Initialiser le SDK en mode hors connexion

Si vous souhaitez écrire des tests entièrement hors connexion, vous pouvez initialiser le SDK sans aucun paramètre :

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

Simuler des valeurs de configuration

Si vous utilisez functions.config() dans le code de vos fonctions, vous pouvez simuler les valeurs de configuration. Par exemple, si functions/index.js contient le code suivant :

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

Vous pouvez ensuite simuler la valeur dans votre fichier de test comme suit :

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

Importer vos fonctions

Pour importer vos fonctions, utilisez require pour importer votre fichier de fonctions principal en tant que module. Veillez à ne le faire qu'après avoir initialisé firebase-functions-test, et simulé les valeurs de configuration.

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

Si vous avez initialisé firebase-functions-test en mode hors connexion et que vous avez admin.initializeApp() dans le code de vos fonctions, vous devez le simuler avant d'importer vos fonctions :

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

Tester les fonctions d'arrière-plan (non HTTP)

Le processus de test des fonctions non HTTP comprend les étapes suivantes :

  1. Encapsulez la fonction que vous souhaitez tester avec la méthode test.wrap.
  2. Créez des données de test.
  3. Appelez la fonction encapsulée avec les données de test que vous avez créées et tous les champs de contexte d'événement que vous souhaitez spécifier.
  4. Faites des assertions sur le comportement.

Commencez par encapsuler la fonction que vous souhaitez tester. Supposons que vous ayez une fonction dans functions/index.js appelée makeUppercase, que vous souhaitez tester. Écrivez ce qui suit dans functions/test/index.test.js :

// "Wrap" the makeUpperCase function from index.js
const myFunctions = require('../index.js');
const wrapped = test.wrap(myFunctions.makeUppercase);

wrapped est une fonction qui appelle makeUppercase lorsqu'elle est appelée. wrapped comporte deux paramètres :

  1. data (obligatoire) : données à envoyer à makeUppercase. Cela correspond directement au premier paramètre envoyé au gestionnaire de fonctions que vous avez écrit. firebase-functions-test fournit des méthodes pour créer des données personnalisées ou des exemples de données.
  2. eventContextOptions (facultatif) : champs du contexte d'événement que vous souhaitez spécifier. Le contexte d'événement est le deuxième paramètre envoyé au gestionnaire de fonctions que vous avez écrit. Si vous n'incluez pas de paramètre eventContextOptions lorsque vous appelez wrapped, un contexte d'événement est toujours généré avec des champs pertinents. Vous pouvez remplacer certains des champs générés en les spécifiant ici. Notez que vous n'avez qu'à inclure les champs que vous souhaitez remplacer. Tous les champs que vous n'avez pas remplacés sont générés.
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
});

Créer des données de test

Le premier paramètre d'une fonction encapsulée correspond aux données de test permettant d'appeler la fonction sous-jacente. Il existe plusieurs façons de créer des données de test.

Utiliser des données personnalisées

firebase-functions-test comporte plusieurs fonctions permettant de créer les données nécessaires pour tester vos fonctions. Par exemple, utilisez test.firestore.makeDocumentSnapshot pour créer un DocumentSnapshot Firestore. Le premier argument correspond aux données, et le deuxième au chemin d'accès complet de référence, et il existe un troisième argument facultatif pour les autres propriétés de l'instantané que vous pouvez spécifier.

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

Si vous testez une fonction onUpdate ou onWrite, vous devez créer deux instantanés : un pour l'état avant et un pour l'état après. Vous pouvez ensuite utiliser la méthode makeChange pour créer un objet Change avec ces instantanés.

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

Consultez la documentation de référence de l'API pour obtenir des fonctions similaires pour tous les autres types de données.

Utiliser des exemples de données

Si vous n'avez pas besoin de personnaliser les données utilisées dans vos tests, firebase-functions-test propose des méthodes permettant de générer des exemples de données pour chaque type de fonction.

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

Consultez la documentation de référence de l'API pour obtenir des méthodes permettant d'obtenir des exemples de données pour chaque type de fonction.

Utiliser des données simulées (pour le mode hors connexion)

Si vous avez initialisé le SDK en mode hors connexion et que vous testez une Cloud Firestore ou Realtime Database fonction, vous devez utiliser un objet simple avec des stubs au lieu de créer un DocumentSnapshot ou DataSnapshot.

Supposons que vous écriviez un test unitaire pour la fonction suivante :

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

Dans la fonction, snap est utilisé deux fois :

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

Dans le code de test, créez un objet simple dans lequel ces deux chemins de code fonctionneront, et utilisez Sinon pour simuler les méthodes.

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

Faire des assertions

Après avoir initialisé le SDK, encapsulé les fonctions et créé des données, vous pouvez appeler les fonctions encapsulées avec les données créées et faire des assertions sur le comportement. Vous pouvez utiliser une bibliothèque telle que Chai pour effectuer ces assertions.

Faire des assertions en mode en ligne

Si vous avez initialisé le Firebase Test SDK pour Cloud Functions en mode en ligne, vous pouvez affirmer que les actions souhaitées (telles qu'une écriture dans la base de données) ont eu lieu en utilisant le SDK firebase-admin.

L'exemple ci-dessous affirme que "INPUT" a été écrit dans la base de données du projet de test.

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

Faire des assertions en mode hors connexion

Vous pouvez faire des assertions sur la valeur de retour attendue de la fonction :

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

Vous pouvez également utiliser des espions Sinon pour affirmer que certaines méthodes ont été appelées, et avec les paramètres attendus.

Tester les fonctions HTTP

Pour tester les fonctions HTTP onCall, utilisez la même approche que pour tester les fonctions d'arrière-plan.

Si vous testez des fonctions HTTP onRequest, vous devez utiliser firebase-functions-test si :

  • Vous utilisez functions.config().
  • Votre fonction interagit avec un projet Firebase ou d'autres API Google, et vous souhaitez utiliser un projet Firebase réel et ses identifiants pour vos tests.

Une fonction HTTP onRequest comporte deux paramètres : un objet de requête et un objet de réponse. Voici comment tester l'exemple de fonction addMessage():

  • Remplacez la fonction de redirection dans l'objet de réponse, car sendMessage() l'appelle.
  • Dans la fonction de redirection, utilisez chai.assert pour vous aider à faire des assertions sur les paramètres avec lesquels la fonction de redirection doit être appelée :
// 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);

Nettoyage des tests

À la toute fin de votre code de test, appelez la fonction de nettoyage. Cela supprime les variables d'environnement définies par le SDK lors de son initialisation et supprime les applications Firebase qui ont pu être créées si vous avez utilisé le SDK pour créer un DataSnapshot de base de données en temps réel ou un DocumentSnapshot Firestore.

test.cleanup();

Consulter des exemples complets et en savoir plus

Vous pouvez consulter les exemples complets dans le dépôt GitHub Firebase.

Pour en savoir plus, consultez la documentation de référence de l'API pour firebase-functions-test.