Tests unitaires des fonctions Cloud

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

  • S'occupe de la configuration et du démontage appropriés pour vos tests, tels que la définition et la suppression des variables d'environnement nécessaires à firebase-functions .
  • Génère des exemples de données et un contexte d'événement, de sorte que vous n'ayez qu'à spécifier les champs pertinents pour votre test.

Configuration des tests

Installez à la fois 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 un nouveau fichier à l'intérieur pour votre code de test et nommez-le quelque chose comme index.test.js .

Enfin, modifiez functions/package.json pour ajouter ce qui suit :

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

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

Initialisation du SDK de test Firebase pour les fonctions cloud

Il existe deux manières d'utiliser firebase-functions-test :

  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 par l'utilisateur, 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 ligne : écrivez des tests unitaires cloisonnés et hors ligne sans effets secondaires. Cela signifie que tous les appels de méthode qui interagissent avec un produit Firebase (par exemple, écrire dans la base de données ou créer un utilisateur) doivent être remplacés. L'utilisation du mode hors ligne est généralement déconseillée si vous disposez des fonctions Cloud Firestore ou Realtime Database, 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 du projet nécessaires à l'initialisation de l'application via firebase-admin et 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 souhaitée.
  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 pour afficher les valeurs de configuration.

Pour créer un fichier clé :

  1. Ouvrez le volet Comptes de service de la console Google Cloud.
  2. Sélectionnez le compte de service App Engine par défaut et utilisez le menu d'options à droite pour sélectionner Créer une clé .
  3. Lorsque vous y êtes invité, sélectionnez JSON pour le 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 ligne

Si vous souhaitez écrire des tests complètement hors ligne, vous pouvez initialiser le SDK sans aucun paramètre :

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

Valeurs de configuration moqueuses

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

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

Ensuite, vous pouvez vous moquer de la valeur contenue dans votre fichier de test comme ceci :

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

Importer vos fonctions

Pour importer vos fonctions, utilisez require pour importer votre fichier de fonctions principales 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 ligne et que vous avez admin.initializeApp() dans le code de vos fonctions, vous devez le stub 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');

Test des fonctions en arrière-plan (non HTTP)

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

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

Enveloppez d’abord la fonction que vous souhaitez tester. Disons que vous avez 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 invoque makeUppercase lorsqu'elle est appelée. wrapped prend 2 paramètres :

  1. data (obligatoire) : les 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 construire 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 de l'é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 lors de l'appel wrapped , un contexte d'événement est toujours généré avec des champs sensibles. Vous pouvez remplacer certains champs générés en les spécifiant ici. Notez que vous devez uniquement 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
});

Construire des données de test

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

Utiliser des données personnalisées

firebase-functions-test possède 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 Firestore DocumentSnapshot . Le premier argument correspond aux données, et le deuxième argument est le chemin de référence complet, et il existe un troisième argument facultatif pour d'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 devrez créer deux instantanés : un pour l'état avant et un pour l'état après. Ensuite, vous pouvez 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 référence API pour 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 pour 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 référence API pour connaître les méthodes permettant d’obtenir des exemples de données pour chaque type de fonction.

Utilisation de données stubbées (pour le mode hors ligne)

Si vous avez initialisé le SDK en mode hors ligne et testez une fonction Cloud Firestore ou Realtime Database, vous devez utiliser un objet simple avec des stubs au lieu de créer un véritable 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);
    });

À l’intérieur de 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 remplacer 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 affirmations

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

Faire des assertions en mode en ligne

Si vous avez initialisé le SDK de test Firebase 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 à 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');
  });
});

Faire des assertions en mode hors ligne

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 assert.equal(wrapped(snap), true);

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

Test des 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 les 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 vrai projet Firebase et ses identifiants pour vos tests.

Une fonction HTTP onRequest prend 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, puisque 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);

Test de nettoyage

À la toute fin de votre code de test, appelez la fonction de nettoyage. Cela désactive les variables d'environnement définies par le SDK lors de son initialisation et supprime les applications Firebase qui peuvent avoir été 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();

Consultez des exemples complets et apprenez-en davantage

Vous pouvez consulter les exemples complets sur le référentiel Firebase GitHub.

Pour en savoir plus, reportez-vous à la référence API pour firebase-functions-test .