Pruebas unitarias de funciones en la nube

Esta página describe las mejores prácticas y herramientas para escribir pruebas unitarias para sus funciones, como pruebas que serían parte de un sistema de integración continua (CI). Para facilitar las pruebas, Firebase proporciona el SDK de prueba de Firebase para Cloud Functions. Se distribuye en npm como firebase-functions-test y es un SDK de prueba complementario de firebase-functions . El SDK de prueba de Firebase para funciones en la nube:

  • Se encarga de la configuración y desmontaje adecuados para sus pruebas, como configurar y desarmar las variables de entorno que necesitan las firebase-functions .
  • Genera datos de muestra y contexto de eventos, de modo que solo tenga que especificar los campos que sean relevantes para su prueba.

Configuración de prueba

Instale firebase-functions-test y Mocha , un marco de prueba, ejecutando los siguientes comandos en su carpeta de funciones:

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

Luego cree una carpeta test dentro de la carpeta de funciones, cree un nuevo archivo dentro de ella para su código de prueba y asígnele un nombre como index.test.js .

Finalmente, modifique functions/package.json para agregar lo siguiente:

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

Una vez que haya escrito las pruebas, puede ejecutarlas ejecutando npm test dentro de su directorio de funciones.

Inicializando el SDK de prueba de Firebase para funciones en la nube

Hay dos formas de utilizar firebase-functions-test :

  1. Modo en línea (recomendado): escriba pruebas que interactúen con un proyecto de Firebase dedicado a las pruebas para que las escrituras en la base de datos, las creaciones de usuarios, etc. realmente sucedan, y su código de prueba pueda inspeccionar los resultados. Esto también significa que otros SDK de Google utilizados en sus funciones también funcionarán.
  2. Modo sin conexión: escriba pruebas unitarias aisladas y sin conexión sin efectos secundarios. Esto significa que cualquier llamada a método que interactúe con un producto de Firebase (por ejemplo, escribir en la base de datos o crear un usuario) debe bloquearse. Por lo general, no se recomienda usar el modo fuera de línea si tiene funciones de Cloud Firestore o Realtime Database, ya que aumenta en gran medida la complejidad de su código de prueba.

Inicializar SDK en modo en línea (recomendado)

Si desea escribir pruebas que interactúen con un proyecto de prueba, debe proporcionar los valores de configuración del proyecto necesarios para inicializar la aplicación a través de firebase-admin y la ruta a un archivo de clave de cuenta de servicio.

Para obtener los valores de configuración de tu proyecto de Firebase:

  1. Abra la configuración de su proyecto en Firebase console .
  2. En Tus aplicaciones, selecciona la aplicación deseada.
  3. En el panel derecho, seleccione la opción para descargar un archivo de configuración para aplicaciones de Apple y Android.

    Para aplicaciones web, seleccione Configuración para mostrar los valores de configuración.

Para crear un archivo de clave:

  1. Abra el panel Cuentas de servicio de la consola de Google Cloud.
  2. Seleccione la cuenta de servicio predeterminada de App Engine y use el menú de opciones a la derecha para seleccionar Crear clave .
  3. Cuando se le solicite, seleccione JSON para el tipo de clave y haga clic en Crear .

Después de guardar el archivo clave, inicialice el 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');

Inicializar SDK en modo fuera de línea

Si desea escribir pruebas completamente fuera de línea, puede inicializar el SDK sin ningún parámetro:

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

Valores de configuración burlones

Si usa functions.config() en su código de funciones, puede burlarse de los valores de configuración. Por ejemplo, si functions/index.js contiene el siguiente código:

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

Luego puedes simular el valor dentro de tu archivo de prueba de esta manera:

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

Importando tus funciones

Para importar sus funciones, use require para importar su archivo de funciones principal como módulo. Asegúrese de hacer esto solo después de inicializar firebase-functions-test y burlarse de los valores de configuración.

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

Si inicializaste firebase-functions-test en modo fuera de línea y tienes admin.initializeApp() en tu código de funciones, entonces debes codificarlo antes de importar tus funciones:

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

Prueba de funciones en segundo plano (no HTTP)

El proceso para probar funciones que no son HTTP implica los siguientes pasos:

  1. Envuelva la función que le gustaría probar con el método test.wrap
  2. Construir datos de prueba
  3. Invoque la función empaquetada con los datos de prueba que construyó y cualquier campo de contexto de evento que desee especificar.
  4. Hacer afirmaciones sobre el comportamiento.

Primero ajuste la función que desea probar. Digamos que tienes una función en functions/index.js llamada makeUppercase que te gustaría probar. Escribe lo siguiente en functions/test/index.test.js

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

wrapped es una función que invoca makeUppercase cuando se llama. wrapped toma 2 parámetros:

  1. datos (obligatorio): los datos a enviar a makeUppercase . Esto corresponde directamente al primer parámetro enviado al controlador de funciones que escribió. firebase-functions-test proporciona métodos para construir datos personalizados o datos de ejemplo.
  2. eventContextOptions (opcional): campos del contexto del evento que desea especificar. El contexto del evento es el segundo parámetro enviado al controlador de funciones que escribió. Si no incluye un parámetro eventContextOptions al llamar wrapped , aún se genera un contexto de evento con campos sensibles. Puede anular algunos de los campos generados especificándolos aquí. Tenga en cuenta que solo debe incluir los campos que desea anular. Se generan todos los campos que no anuló.
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
});

Construyendo datos de prueba

El primer parámetro de una función empaquetada son los datos de prueba para invocar la función subyacente. Hay varias formas de construir datos de prueba.

Usando datos personalizados

firebase-functions-test tiene varias funciones para construir los datos necesarios para probar sus funciones. Por ejemplo, use test.firestore.makeDocumentSnapshot para crear un DocumentSnapshot de Firestore. El primer argumento son los datos, el segundo argumento es la ruta de referencia completa y hay un tercer argumento opcional para otras propiedades de la instantánea que puede especificar.

// 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 está probando una función onUpdate o onWrite , deberá crear dos instantáneas: una para el estado anterior y otra para el estado posterior. Luego, puede utilizar el método makeChange para crear un objeto Change con estas instantáneas.

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

Consulte la referencia de API para funciones similares para todos los demás tipos de datos.

Usando datos de ejemplo

Si no necesita personalizar los datos utilizados en sus pruebas, firebase-functions-test ofrece métodos para generar datos de ejemplo para cada tipo de función.

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

Consulte la referencia de API para conocer métodos para obtener datos de ejemplo para cada tipo de función.

Uso de datos fragmentados (para el modo fuera de línea)

Si inicializó el SDK en modo fuera de línea y está probando una función de Cloud Firestore o Realtime Database, debe usar un objeto simple con códigos auxiliares en lugar de crear un DocumentSnapshot o DataSnapshot real.

Supongamos que está escribiendo una prueba unitaria para la siguiente función:

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

Dentro de la función, snap se usa dos veces:

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

En el código de prueba, cree un objeto simple donde funcionarán ambas rutas de código y use Sinon para eliminar los métodos.

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

Hacer afirmaciones

Después de inicializar el SDK, empaquetar las funciones y construir datos, puede invocar las funciones empaquetadas con los datos construidos y hacer afirmaciones sobre el comportamiento. Puede utilizar una biblioteca como Chai para hacer estas afirmaciones.

Hacer afirmaciones en modo online

Si inicializó el SDK de prueba de Firebase para Cloud Functions en modo en línea , puede afirmar que se realizaron las acciones deseadas (como una escritura en la base de datos) utilizando el SDK firebase-admin .

El siguiente ejemplo afirma que 'ENTRADA' se ha escrito en la base de datos del proyecto de prueba.

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

Hacer afirmaciones en modo fuera de línea

Puede hacer afirmaciones sobre el valor de retorno esperado de la función:

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

También puede utilizar espías de Sinon para afirmar que se han llamado ciertos métodos y con los parámetros que espera.

Prueba de funciones HTTP

Para probar las funciones HTTP onCall, utilice el mismo enfoque que para probar las funciones en segundo plano .

Si está probando funciones HTTP onRequest, debe usar firebase-functions-test si:

  • Usas functions.config()
  • Su función interactúa con un proyecto de Firebase u otras API de Google y le gustaría utilizar un proyecto de Firebase real y sus credenciales para sus pruebas.

Una función HTTP onRequest toma dos parámetros: un objeto de solicitud y un objeto de respuesta. Así es como puedes probar la función de ejemplo addMessage() :

  • Anule la función de redireccionamiento en el objeto de respuesta, ya que sendMessage() la llama.
  • Dentro de la función de redirección, use chai.assert para ayudar a hacer afirmaciones sobre con qué parámetros se debe llamar a la función de redirección:
// 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);

limpieza de prueba

Al final de su código de prueba, llame a la función de limpieza. Esto desarma las variables de entorno que el SDK configuró cuando se inicializó y elimina las aplicaciones de Firebase que pueden haberse creado si usó el SDK para crear una base de datos en tiempo real DataSnapshot o Firestore DocumentSnapshot .

test.cleanup();

Revise ejemplos completos y aprenda más

Puedes revisar los ejemplos completos en el repositorio de Firebase GitHub.

Para obtener más información, consulta la referencia de API para firebase-functions-test .