Teste de unidade do Cloud Functions

Esta página descreve as melhores práticas e ferramentas para escrever testes de unidade para suas funções, como testes que fariam parte de um sistema de Integração Contínua (CI). Para facilitar os testes, o Firebase fornece o SDK de teste do Firebase para Cloud Functions. Ele é distribuído no npm como firebase-functions-test e é um SDK de teste complementar para firebase-functions . O SDK de teste do Firebase para Cloud Functions:

  • Cuida da configuração e desmontagem apropriadas para seus testes, como configurar e desativar variáveis ​​de ambiente necessárias para firebase-functions .
  • Gera dados de amostra e contexto de evento, para que você só precise especificar os campos relevantes para o seu teste.

Configuração de teste

Instale firebase-functions-test e Mocha , uma estrutura de testes, executando os seguintes comandos em sua pasta de funções:

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

Em seguida, crie uma pasta test dentro da pasta de funções, crie um novo arquivo dentro dela para seu código de teste e nomeie-o como index.test.js .

Por fim, modifique functions/package.json para adicionar o seguinte:

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

Depois de escrever os testes, você pode executá-los executando npm test dentro do diretório de funções.

Inicializando o SDK de teste do Firebase para Cloud Functions

Existem duas maneiras de usar firebase-functions-test :

  1. Modo online (recomendado): escreva testes que interajam com um projeto do Firebase dedicado a testes para que gravações de banco de dados, criações de usuários etc. realmente aconteçam e seu código de teste possa inspecionar os resultados. Isso também significa que outros SDKs do Google usados ​​em suas funções também funcionarão.
  2. Modo offline: escreva testes de unidade isolados e offline sem efeitos colaterais. Isso significa que qualquer chamada de método que interaja com um produto Firebase (por exemplo, gravação no banco de dados ou criação de um usuário) precisa ser stub. Geralmente, o uso do modo off-line não é recomendado se você tiver funções do Cloud Firestore ou do Realtime Database, pois aumenta muito a complexidade do seu código de teste.

Inicialize o SDK no modo online (recomendado)

Se quiser escrever testes que interajam com um projeto de teste, você precisará fornecer os valores de configuração do projeto necessários para inicializar o aplicativo por meio de firebase-admin e o caminho para um arquivo de chave da conta de serviço.

Para obter os valores de configuração do seu projeto Firebase:

  1. Abra as configurações do seu projeto no console do Firebase .
  2. Em Seus aplicativos, selecione o aplicativo desejado.
  3. No painel direito, selecione a opção de baixar um arquivo de configuração para aplicativos Apple e Android.

    Para aplicativos web, selecione Config para exibir valores de configuração.

Para criar um arquivo de chave:

  1. Abra o painel Contas de serviço do console do Google Cloud.
  2. Selecione a conta de serviço padrão do App Engine e use o menu de opções à direita para selecionar Criar chave .
  3. Quando solicitado, selecione JSON para o tipo de chave e clique em Criar .

Depois de salvar o arquivo-chave, inicialize o 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');

Inicialize o SDK no modo offline

Se quiser escrever testes completamente offline, você pode inicializar o SDK sem nenhum parâmetro:

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

Simulando valores de configuração

Se você usar functions.config() em seu código de funções, poderá simular os valores de configuração. Por exemplo, se functions/index.js contiver o seguinte código:

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

Então você pode simular o valor dentro do seu arquivo de teste assim:

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

Importando suas funções

Para importar suas funções, use require para importar seu arquivo de funções principais como um módulo. Certifique-se de fazer isso somente após inicializar firebase-functions-test e simular valores de configuração.

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

Se você inicializou firebase-functions-test no modo offline e tem admin.initializeApp() em seu código de funções, será necessário fazer um stub antes de importar suas funções:

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

Testando funções em segundo plano (não HTTP)

O processo para testar funções não HTTP envolve as seguintes etapas:

  1. Envolva a função que você gostaria de testar com o método test.wrap
  2. Construir dados de teste
  3. Invoque a função encapsulada com os dados de teste que você construiu e quaisquer campos de contexto de evento que você gostaria de especificar.
  4. Faça afirmações sobre o comportamento.

Primeiro envolva a função que você deseja testar. Digamos que você tenha uma função em functions/index.js chamada makeUppercase , que gostaria de testar. Escreva o seguinte em functions/test/index.test.js

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

wrapped é uma função que invoca makeUppercase quando é chamada. wrapped leva 2 parâmetros:

  1. data (obrigatório): os dados a serem enviados para makeUppercase . Isso corresponde diretamente ao primeiro parâmetro enviado ao manipulador de função que você escreveu. firebase-functions-test fornece métodos para construir dados personalizados ou dados de exemplo.
  2. eventContextOptions (opcional): campos do contexto do evento que você deseja especificar. O contexto do evento é o segundo parâmetro enviado ao manipulador de função que você escreveu. Se você não incluir um parâmetro eventContextOptions ao chamar wrapped , um contexto de evento ainda será gerado com campos sensíveis. Você pode substituir alguns dos campos gerados especificando-os aqui. Observe que você só precisa incluir os campos que deseja substituir. Todos os campos que você não substituiu serão gerados.
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
});

Construindo dados de teste

O primeiro parâmetro de uma função encapsulada são os dados de teste com os quais invocar a função subjacente. Existem várias maneiras de construir dados de teste.

Usando dados personalizados

firebase-functions-test possui diversas funções para construir os dados necessários para testar suas funções. Por exemplo, use test.firestore.makeDocumentSnapshot para criar um Firestore DocumentSnapshot . O primeiro argumento são os dados, e o segundo argumento é o caminho de referência completo, e há um terceiro argumento opcional para outras propriedades do instantâneo que você pode 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);

Se estiver testando uma função onUpdate ou onWrite , você precisará criar dois instantâneos: um para o estado anterior e outro para o estado posterior. Então, você pode usar o método makeChange para criar um objeto Change com esses instantâneos.

// 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 a referência da API para funções semelhantes para todos os outros tipos de dados.

Usando dados de exemplo

Se você não precisar personalizar os dados usados ​​em seus testes, o firebase-functions-test oferece métodos para gerar dados de exemplo para cada tipo de função.

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

Consulte a referência da API para métodos de obtenção de dados de exemplo para cada tipo de função.

Usando dados stub (para modo offline)

Se você inicializou o SDK no modo offline e está testando uma função do Cloud Firestore ou do Realtime Database, use um objeto simples com stubs em vez de criar um DocumentSnapshot ou DataSnapshot real.

Digamos que você esteja escrevendo um teste unitário para a seguinte função:

// 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 da função, snap é usado duas vezes:

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

No código de teste, crie um objeto simples onde ambos os caminhos de código funcionarão e use Sinon para fazer stub dos 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);

Fazendo afirmações

Depois de inicializar o SDK, agrupar as funções e construir os dados, você poderá invocar as funções agrupadas com os dados construídos e fazer afirmações sobre o comportamento. Você pode usar uma biblioteca como Chai para fazer essas afirmações.

Fazendo afirmações no modo online

Se você inicializou o SDK de teste do Firebase para Cloud Functions no modo on-line , poderá afirmar que as ações desejadas (como uma gravação no banco de dados) ocorreram usando o SDK firebase-admin .

O exemplo abaixo afirma que 'INPUT' foi gravado no banco de dados do projeto de teste.

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

Fazendo afirmações no modo offline

Você pode fazer afirmações sobre o valor de retorno esperado da função:

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

Você também pode usar espiões Sinon para afirmar que certos métodos foram chamados e com os parâmetros que você espera.

Testando funções HTTP

Para testar funções HTTP onCall, use a mesma abordagem do teste de funções em segundo plano .

Se você estiver testando funções HTTP onRequest, use firebase-functions-test se:

  • Você usa functions.config()
  • Sua função interage com um projeto do Firebase ou outras APIs do Google, e você gostaria de usar um projeto real do Firebase e suas credenciais para seus testes.

Uma função HTTP onRequest usa dois parâmetros: um objeto de solicitação e um objeto de resposta. Veja como você pode testar a função de exemplo addMessage() :

  • Substitua a função de redirecionamento no objeto de resposta, pois sendMessage() a chama.
  • Dentro da função de redirecionamento, use chai.assert para ajudar a fazer afirmações sobre quais parâmetros a função de redirecionamento deve ser chamada:
// 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);

Limpeza de teste

No final do seu código de teste, chame a função de limpeza. Isso desativa as variáveis ​​de ambiente que o SDK definiu quando foi inicializado e exclui aplicativos do Firebase que podem ter sido criados se você usou o SDK para criar um banco de dados em tempo real DataSnapshot ou Firestore DocumentSnapshot .

test.cleanup();

Revise exemplos completos e saiba mais

Você pode revisar os exemplos completos no repositório GitHub do Firebase.

Para saber mais, consulte a referência da API firebase-functions-test .