Prueba de unidades de Cloud Functions

En esta página, se describen las prácticas recomendadas y herramientas para escribir pruebas de unidades para tus funciones, como las que formarán parte de un sistema de integración continua (IC). A fin de facilitar las pruebas, Firebase ofrece el SDK de prueba de Firebase para Cloud Functions. Se distribuye en npm como firebase-functions-test y es un SDK de prueba complementario para firebase-functions. El SDK de prueba de Firebase para Cloud Functions hace lo siguiente:

  • Se encarga de las desconexiones y de la configuración adecuada para tus pruebas, como la configuración y desconfiguración de las variables de entorno que necesita firebase-functions.
  • Genera datos de muestra y contexto de eventos a fin de que solo debas especificar los campos relevantes para tu prueba.

Configuración de las pruebas

Instala firebase-functions-test y el marco de trabajo de pruebas Mocha. Para ello, ejecuta los siguientes comandos en la carpeta de funciones:

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

A continuación, crea una carpeta test dentro de la carpeta functions, crea un archivo nuevo para el código de prueba y ponle un nombre como index.test.js.

Por último, modifica functions/package.json para agregar lo siguiente:

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

Después de que hayas escrito las pruebas, ejecuta npm test dentro del directorio de funciones para iniciarlas.

Inicializa el SDK de prueba de Firebase para Cloud Functions

Puedes usar firebase-functions-test de las siguientes dos maneras:

  1. Modo sin conexión: Escribe pruebas de unidades sin conexión y aisladas sin efectos secundarios. Esto significa que se deben usar stubs en todos los métodos que activen una interacción con productos de Firebase (p. ej., escribir en la base de datos o crear un usuario).
  2. Modo en línea: Escribe pruebas que interactúen con un proyecto de Firebase dedicado a realizar pruebas, a fin de que las escrituras en la base de datos, las creaciones de usuarios, entre otras acciones, ocurran sin problemas y que tu código de pruebas pueda revisar los resultados. Esto significa que también funcionarán los otros SDK de Google que uses en tus funciones.

Inicializa SDK en modo sin conexión

Si quieres escribir pruebas completamente sin conexión, puedes inicializar el SDK sin ningún parámetro:

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

Inicializa el SDK en modo en línea

Si quieres escribir pruebas que interactúen con un proyecto de prueba, debes incluir los valores de configuración del proyecto que sean necesarios para inicializar la app mediante firebase-admin y la ruta de acceso al archivo de clave de una cuenta de servicio.

Sigue estos pasos para obtener los valores de configuración de tu proyecto de Firebase:

  1. Ve a Firebase Console.
  2. Selecciona tu proyecto y haz clic en Agregar Firebase a tu app web. Los valores de configuración aparecerán en la ventana emergente.

Sigue estos pasos para crear un archivo de clave:

  1. Abre el panel de cuentas de servicio de Google Cloud Console.
  2. Selecciona la cuenta de servicio predeterminada para App Engine y selecciona la opción Crear clave en el menú de opciones de la derecha.
  3. Cuando se te solicite, selecciona JSON para el tipo de clave y haz clic en Crear.

Después de guardar el archivo de claves, inicializa el SDK de la siguiente manera:

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

Simula los valores de configuración

Si usas functions.config() en el código de las funciones, tienes la opción de simular 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;

En ese caso, podrás simular los valores internos de tu archivo de prueba de la siguiente manera:

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

Importa funciones

Para importar tus funciones, usa require a fin de importar el archivo principal como un módulo. Asegúrate de hacer esto después de inicializar firebase-functions-test y simular 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 sin conexión y tienes admin.initializeApp() en el código de tus funciones, debes usar un stub antes de importarlas, como se muestra en el siguiente ejemplo:

// 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 las funciones en segundo plano (que no son HTTP)

El proceso para probar las funciones en segundo plano que no son HTTP requiere los siguientes pasos:

  1. Une la función que deseas probar con el método test.wrap.
  2. Crea datos de prueba
  3. Invoca la función seleccionada con los datos de prueba que creaste y todos los campos de contexto de eventos que deseas especificar.
  4. Realiza aserciones sobre el comportamiento

Primero, une la función que quieres probar. Supongamos que tienes una función en functions/index.js que se llama makeUppercase y que quieres 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 a makeUppercase cuando se la llama. wrapped toma estos 2 parámetros:

  1. data (obligatorio): Los datos que se enviarán a makeUppercase. Esto corresponde directamente al primer parámetro que se envía al controlador de la función que escribiste. firebase-functions-test proporciona métodos para crear datos personalizados o de ejemplo.
  2. eventContextOptions (opcional): Los campos del contexto de eventos que quieres especificar. El contexto de eventos es el segundo parámetro que se envía al controlador de la función que escribiste. Si no incluyes un parámetro eventContextOptions cuando llamas a wrapped, se generará un contexto de evento con campos confidenciales. Puedes especificar aquí algunos de los campos que se generan para anularlos. Ten en cuenta que solo debes incluir los campos que deseas anular. Se generarán todos los campos que no anules.
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
});

Crea datos de prueba

Los datos de prueba son el primer parámetro de una función unida con los que se invocará la función subyacente. Existen varias formas de crear datos de prueba.

Usa datos personalizados

firebase-functions-test tiene varias funciones que permiten crear los datos necesarios para realizar las pruebas. Por ejemplo, usa test.firestore.makeDocumentSnapshot para crear una DocumentSnapshot de Firestore. El primer argumento son los datos y el segundo es la ruta de referencia completa. Además, hay un tercer argumento opcional que corresponde a otras propiedades de la instantánea que puedes 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 pruebas la función onUpdate o la función onWrite, deberás crear dos instantáneas: una para el estado inicial y otra para el final. Luego, puedes usar 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);

Si deseas conocer funciones similares para todos los otros tipos de datos, consulta la referencia de la API.

Usa datos de ejemplo

Si no necesitas personalizar los datos que se usan en las pruebas, firebase-functions-test ofrece métodos para generar datos de ejemplos 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();

Consulta la referencia de la API a fin de ver cómo obtener datos de ejemplo para cada tipo de función.

Usa datos con stub (para el modo sin conexión)

Si inicializaste el SDK en modo sin conexión y pruebas una función de Firestore o Database, es posible que te resulte más fácil usar un objeto plano con stubs que crear una DocumentSnapshot o DataSnapshot para luego usar stubs en sus métodos.

Supongamos que escribes una prueba de unidades 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();
      console.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);
    });

snap se usa dos veces dentro de la función:

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

En el código de prueba, crea un objeto plano en el que funcionen las rutas de acceso de ambos códigos y utiliza Sinon para usar un stub en 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);

Realiza aserciones

Después de inicializar el SDK, unir las funciones y crear los datos, puedes usar estos últimos para invocar las funciones y realizar aserciones sobre el comportamiento. Para hacerlas, puedes usar una biblioteca como Chai.

Cómo realizar declaraciones en el modo sin conexión

Puedes realizar aserciones sobre los valores que esperas obtener 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 puedes usar Sinon spies para confirmar que se llamaron ciertos métodos, junto con los parámetros que esperas.

Realiza aserciones en el modo en línea

Si inicializaste el SDK de prueba de Firebase para Cloud Functions en modo en línea, puedes confirmar que se realizaron las acciones deseadas (como la escritura en la base de datos) mediante el SDK de firebase-admin.

En el siguiente ejemplo se indica que se escribió “INPUT” en la base de datos en tiempo real 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');
  });
});

Prueba funciones de HTTP

Si pruebas funciones de HTTP onRequest, debes usar firebase-functions-test en los siguientes casos:

  • Usas functions.config()
  • Si tu función interactúa con un proyecto de Firebase o con otras API de Google y deseas realizar tus pruebas con un proyecto real de Firebase y sus credenciales

Una función de HTTP onRequest requiere dos parámetros: un objeto de solicitud y uno de respuesta. Podrías probar la función de ejemplo de addMessage() de la siguiente manera:

  • Anula la función de redireccionamiento en el objeto de respuesta, ya que la llama sendMessage().
  • Dentro de la función de redireccionamiento, usa chai.assert para ayudarte a realizar aserciones sobre los parámetros con los que se debería llamar a la función de redireccionamiento:
// 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 la prueba

En la parte final de tu código de prueba, llama a la función de limpieza. Esto quita las variables de entorno que SDK configuró cuando se inicializó y borra las apps de Firebase que pudieron haberse instalado si usaste el SDK para crear una DocumentSnapshot de Firestore o DataSnapshot de base de datos en tiempo real.

test.cleanup();

Revisa los ejemplos completos y obtén más información

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

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