Ir a la consola

Prueba de unidades de Cloud Functions

En esta página, se describen las recomendaciones y herramientas para escribir pruebas de unidades para tus funciones, como las que formarán parte de un sistema de Integración continua (CI). 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 del ambiente que firebase-functions necesita.
  • Genera datos de muestra y contexto de eventos a fin de que solo debas especificar los campos relevantes para tu prueba.

Configuración de la prueba

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 y agrega lo siguiente:

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

Cuando hayas escrito las pruebas, ejecuta el archivo npm test que se encuentra en tu carpeta functions para iniciarlas.

Cómo inicializar el SDK de prueba de Firebase para Cloud Functions

Existen dos formas de usar firebase-functions-test:

  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 al archivo de clave de una cuenta de servicio.

Cómo 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.

Para crear un archivo de clave, haz lo siguiente:

  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 como se indica a continuación:

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

Cómo importar tus 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');

Cómo probar 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 declaraciones acerca del comportamiento.

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

  1. data (obligatorio): Corresponde a 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 ofrece métodos para crear datos personalizados o de ejemplo.
  2. eventContextOptions (opcional): los campos del contexto de eventos que deseas 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
});

Cómo crear 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.

Con 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 API.

Con datos de ejemplo

Si no necesitas personalizar los datos que se usan en tus 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 API para ver cómo obtener datos de ejemplo para cada tipo de función.

Con 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 unidad 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 en 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 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);

Cómo realizar declaraciones

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

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

Puedes realizar declaraciones 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 declarar que se llamaron ciertos métodos, junto con los parámetros que esperas.

Cómo realizar declaraciones en el modo en línea

Si inicializaste el SDK de prueba de Firebase para Cloud Functions en modo en línea, puedes declarar 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:

  • Si 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 HTTP onRequest requiere dos parámetros: un objeto de solicitud y uno de respuesta. Podrías probar la función de ejemplo addMessage() de la siguiente manera:

  • Anula la función de redirección en el objeto de respuesta, ya que sendMessage() la llama.
  • Dentro de la función de redireccionamiento, usa chai.assert para ayudarte a realizar declaraciones acerca de 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 del ambiente 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.

Consulta la referencia de API para firebase-functions-test a fin de obtener más información.