Teste de unidade do Cloud Functions

Esta página descreve as práticas recomendadas e as ferramentas para escrever testes de unidade para suas funções, como testes que poderiam fazer parte de um sistema de Integração Contínua (CI, na sigla em inglês). Para facilitar esse processo, o Firebase fornece o SDK de teste do Firebase para o 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 o Cloud Functions:

  • Cuida da configuração e da eliminação apropriada para seus testes, como a definição e desconfiguração das variáveis de ambiente necessárias para firebase-functions.
  • Gera dados de amostra e contextos de evento para que seja necessário especificar apenas os campos relevantes ao teste.

Como configurar o teste

Instale o firebase-functions-test e Mocha, um framework de teste, executando os seguintes comandos na 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 dele para o código de teste e dê a ele um nome semelhante a index.test.js.

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

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

Depois de escrever os testes, é possível executá-los executando npm test dentro do diretório de funções.

Como inicializar o SDK de teste do Firebase para o Cloud Functions

Há duas maneiras de usar firebase-functions-test:

  1. Modo on-line (recomendado): escreva testes que interajam com um projeto do Firebase dedicado a testes para que gravações de banco de dados, criação de usuários e ações similares realmente aconteçam e seu código de teste possa inspecionar os resultados. Isso também significa que outros SDKs do Google usados nas suas funções também funcionarão.
  2. Modo off-line: escreva testes de unidade off-line e em silos sem efeitos colaterais. Isso significa que todas as chamadas de método que interagem com um produto do Firebase (por exemplo, uma gravação no banco de dados ou a criação de um usuário) precisam ser fragmentadas. Geralmente, não é recomendado usar o modo off-line se você tiver funções do Cloud Firestore ou do Realtime Database, já que isso aumenta muito a complexidade do código de teste.

Inicializar o SDK no modo on-line (recomendado)

Para criar testes que interajam com um projeto de teste, envie os valores de configuração exigidos para inicializar o app usando firebase-admin. Também é preciso enviar o caminho para um arquivo de chave da conta de serviço.

Para acessar os valores de configuração do seu projeto do Firebase, siga as etapas a seguir:

  1. Abra as configurações do projeto no Console do Firebase.
  2. Em Apps, selecione o app desejado.
  3. No painel à direita, selecione a opção para fazer o download de um arquivo de configuração para apps Apple e Android.

    Em apps da Web, selecione Configuração para exibir os valores de configuração.

Para criar um arquivo de chave, siga as etapas a seguir:

  1. Abra o painel de 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 a opção Criar chave.
  3. Selecione JSON para a opção do tipo de chave e clique em Criar.

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

Como inicializar o SDK no modo off-line

Se quiser escrever testes completamente off-line, você poderá inicializar o SDK sem nenhum parâmetro:

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

Como simular valores de configuração

Se você usar functions.config() no 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;

Você poderá simular o valor que está no arquivo de teste da seguinte forma:

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

Como importar suas funções

Para importar suas funções, use require para importar o arquivo de funções principais como um módulo. Certifique-se de fazer isso somente depois de inicializar firebase-functions-test e simular os 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 off-line e tem admin.initializeApp() no seu código de funções, faça um stub para ele 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');

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

Para testar funções que não são HTTP, siga estas etapas:

  1. Encapsule a função que você gostaria de testar com o método test.wrap.
  2. Construa dados de teste para a função.
  3. Chame a função encapsulada com os dados de teste que você construiu e qualquer campo de contexto de evento que você gostaria de especificar.
  4. Faça declarações sobre o comportamento.

Encapsule a função que gostaria de testar. Digamos que você tenha uma função em functions/index.js chamada makeUppercase que gostaria de testar. Escreva o seguinte código no 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 requer dois parâmetros:

  1. data (obrigatório): os dados a serem enviados a makeUppercase. Isso corresponde diretamente ao primeiro parâmetro enviado ao gerenciador de funções 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ê gostaria de especificar. O contexto do evento é o segundo parâmetro enviado ao administrador de funções que você escreveu. Se você não incluir um parâmetro eventContextOptions ao chamar wrapped, um contexto do evento ainda será gerado com campos relevantes. Você pode modificar alguns dos campos gerados, especificando-os aqui. Observe que você só precisa incluir os campos que gostaria de modificar. Todos os campos que você não modificou sã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
});

Como criar dados de teste

O primeiro parâmetro de uma função encapsulada é o conjunto de dados de teste para chamar a função subjacente. Existem várias maneiras de criar dados de teste.

Com usar dados personalizados

firebase-functions-test tem diversas funções para construir dados necessários para testar suas funções. Por exemplo, use test.firestore.makeDocumentSnapshot para criar um Firestore DocumentSnapshot. O primeiro argumento é o conjunto de dados, e o segundo é o caminho de referência completo. Há um terceiro argumento opcional para outras propriedades do snapshot 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 você estiver testando uma função onUpdate ou onWrite, será necessário criar dois snapshots: um para o estado anterior e outro para o após. Em seguida, é possível usar o método makeChange para criar um objeto Change com esses snapshots.

// 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 mais informações sobre funções parecidas de todos os outros tipos de dados.

Como usar dados de exemplo

Se você não precisar personalizar os dados usados nos seus testes, utilize firebase-functions-test 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 os métodos da referência da API e veja dados de exemplo para cada tipo de função.

Como usar dados fragmentados (modo off-line)

Se você inicializou o SDK no modo off-line 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.

Vamos supor que você esteja escrevendo um teste de unidade 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 em que ambos os caminhos de código funcionem e use o Sinon para fragmentar os 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);

Como fazer declarações

Depois de inicializar o SDK, encapsular as funções e criar dados, é possível chamar as funções encapsuladas com os dados construídos e fazer declarações sobre o comportamento. Você pode usar uma biblioteca como a Chai para fazer isso.

Como fazer declarações no modo on-line

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

O exemplo abaixo declara que o valor "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');
  });
});

Como fazer declarações no modo off-line

Você pode fazer declaraçõ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 de teste do Sinon para declarar que alguns métodos foram chamados e que os parâmetros corretos foram usados.

Como testar funções HTTP

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

Caso você esteja testando as funções HTTP onRequest, use o método firebase-functions-test se:

  • você usar functions.config();
  • sua função interage com um projeto do Firebase ou com outras APIs do Google e você gostaria de usar um projeto real do Firebase e suas credenciais para os 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 o exemplo de função addMessage():

  • Substitua a função de redirecionamento no objeto de resposta, já que sendMessage() a chama.
  • Na função de redirecionamento, use chai.assert para fazer declarações sobre quais parâmetros devem ser usados para chamar a função de redirecionamento:
// 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 testes

No final do seu código de teste, chame a função de limpeza. Isso restaura as variáveis de ambiente que o SDK definiu quando foi inicializado e exclui os 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();

Confira os exemplos completos e saiba mais

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

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