Tests unitaires de Cloud Functions

Cette page décrit les bonnes pratiques et les outils à utiliser pour é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 la Firebase Test SDK pour Cloud Functions. Il est distribué sur npm sous le nom firebase-functions-test et constitue un SDK de test associé à firebase-functions. Firebase Test SDK pour Cloud Functions:

  • Gère la configuration et le démontage appropriés pour vos tests, par exemple en définissant et en désactivant 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'ayez qu'à spécifier les champs pertinents pour votre test.

Configuration du test

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 "functions", créez-y un fichier pour votre code de test et nommez-le 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 façons:

  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, la création 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 cloisonné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 fonctions Cloud Firestore ou Realtime Database, car elle 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 du projet nécessaires pour initialiser l'application via firebase-admin, ainsi que le chemin d'accès à un fichier de clé de compte de service.

Pour obtenir les valeurs de configuration de votre projet Firebase:

  1. Ouvrez les paramètres de votre projet dans la console Firebase.
  2. Dans Vos applications,sélectionnez l'application de votre choix.
  3. Dans le volet de droite, sélectionnez l'option permettant de télécharger un fichier de configuration pour les applications Apple et Android.

    Pour les applications Web, sélectionnez Config (Configuration) pour afficher les valeurs de configuration.

Pour créer un fichier de clé:

  1. Ouvrez le volet "Comptes de service" de la console Google Cloud.
  2. Sélectionnez le compte de service par défaut App Engine, puis utilisez le menu d'options à droite pour sélectionner Créer une clé.
  3. Lorsque vous y êtes invité, sélectionnez JSON comme type de clé, puis cliquez sur Créer.

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

Initialiser le SDK en mode hors connexion

Si vous souhaitez écrire des tests complètement 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. Assurez-vous de 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 admin.initializeApp() figure dans le code de vos fonctions, vous devez le remplacer par un bouchon 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 implique les étapes suivantes:

  1. Encapsulez la fonction que vous souhaitez tester avec la méthode test.wrap.
  2. Créer 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. Faire des assertions sur le comportement

Encapsulez d'abord la fonction que vous souhaitez tester. Supposons que vous souhaitiez tester une fonction makeUppercase dans functions/index.js. É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 fonction 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 de l'événement que vous souhaitez spécifier. Le contexte de l'événement est le deuxième paramètre envoyé au gestionnaire de fonction 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 sensibles. Vous pouvez remplacer certains des champs générés en les spécifiant ici. Notez que vous n'avez à inclure que les champs que vous souhaitez remplacer. Tous les champs que vous n'avez pas ignoré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 est les données de test à utiliser pour 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 contient un certain nombre de 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, le deuxième au chemin d'accès complet de la référence, et un troisième argument facultatif permet de spécifier d'autres propriétés de l'instantané.

// 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 connaître les fonctions similaires pour tous les autres types de données.

Utiliser un exemple 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 fictives (pour le mode hors connexion)

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

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, puis utilisez Sinon pour créer des bouchons pour 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 construit des données, vous pouvez appeler les fonctions encapsulées avec les données construites et émettre des assertions sur le comportement. Vous pouvez utiliser une bibliothèque telle que Chai pour effectuer ces assertions.

Effectuer des assertions en mode en ligne

Si vous avez initialisé Firebase Test SDK pour Cloud Functions en mode en ligne, vous pouvez vérifier que les actions souhaitées (telles qu'une écriture dans la base de données) ont eu lieu à l'aide du 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');
  });
});

Émettre des assertions en mode hors connexion

Vous pouvez émettre 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 assert.equal(wrapped(snap), 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 en 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 véritable projet Firebase et ses identifiants pour vos tests.

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

  • Ignorez 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 à émettre 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 de test

À la toute fin de votre code de test, appelez la fonction de nettoyage. Cela réinitialise 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 une base de données en temps réel DataSnapshot ou Firestore DocumentSnapshot.

test.cleanup();

Consulter des exemples complets et en savoir plus

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

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