Testing Cloud Functions for Firebase with Jasmine and Typescript

佐手、 提交于 2021-02-07 11:01:29

问题


I'm currently investigating Google Cloud Functions and have some basic test functions written in typescript.

The functions work as expected and I am now attempting to create unit tests using Jasmine. (I'm not using Chai/sinon as per the docs as the rest of my project uses jasmine).

I have two issues 1) the test does not run due to this error

throw new Error('Firebase config variables are not available. ' + ^ Error: Firebase config variables are not available. Please use the latest version of the Firebase CLI to deploy this function

2) Given the test did run, I'm not sure how to test that the response is as expected.

Index file

import * as functions from 'firebase-functions'

import { helloWorldHandler } from  './functions/hello-world';

export let helloWorld = functions.https.onRequest((req, res) => {
    helloWorldHandler(req, res);
});

File under test

export let helloWorldHandler = (request, response) => {
    response.send("Hello from Firebase Cloud!");
}

Spec

import {} from 'jasmine';
import * as functions from 'firebase-functions'
import { helloWorldHandler } from './hello-world';
import * as endpoints from '../index';

describe('Cloud Functions : Hello World', () => {

    let configStub = {
        firebase: {
            databaseURL: "https://myProject.firebaseio.com",
            storageBucket: "myProject.appspot.com",
        }
    };

    it('should return correct message', () => {

        let spy = spyOn(functions, 'config').and.returnValue(configStub);

        const expected = 'Hello from Firebase Cloud!';
        // A fake request and response objects
        const req : any = {};
        const res : any = { };

        endpoints.helloWorld(req, res);

         //here test response from helloWorld is as expected

      });


});

回答1:


If you are writing unit tests, then you don't want to test third party APIs. Thus, the goal should be to isolate your code logic and test that. End-to-end tests are best suited for regression testing your integrations.

So the first step here would be to remove tools like firebase-functions and the Database SDKs from the picture (as much as this is reasonable). I accomplished this by separating my libs from the functions logic like so:

// functions/lib/http.js
exports.httpFunction = (req, res) => {
   res.send(`Hello ${req.data.foo}`);
};

// functions/index.js
const http = require('lib/http');
const functions = require('firebase-functions');

// we have decoupled the Functions invocation from the method
// so the method can be tested without including the functions lib!
functions.https.onRequest(http.httpFunction);

Now I have nicely isolated logic that I can test via a unit test. I mock any arguments that would be passed into my methods, removing third party APIs from the picture.

So here's what my unit tests in Jasmine look like:

// spec/lib/http.spec.js
const http = require('../functions/lib/http');

describe('functions/lib/http', () => {
   expect('send to be called with "hello world"', () => {
      // first thing to do is mock req and res objects
      const req = {data: {foo: 'world'}};
      const res = {send: (s) => {});

      // now let's monitor res.send to make sure it gets called
      spyOn(res, 'send').and.callThrough();

      // now run it
      http.httpFunction(req, res);

      // new test it
      expect(res.send).toHaveBeenCalledWith("Hello world");
   });
});

There are a lot of complexities with testing third party libs. The best answer here to apply TDD/BDD principles early and abstract third party libs into services that can easily be mocked.

For example, if I were interacting with Firebase Admin within my functions, I could easily end up with a method that has lots of third party dependencies to contend with:

// functions/lib/http.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const env = require('./env');
const serviceAccount = require(env.serviceAccountPath);

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: `https://${env.dbUrl}.firebaseio.com`
});

exports.httpFunction = (req, res) => {
   let path = null;
   let data = null;

   // this is what I really want to test--my logic!
   if( req.query.foo ) {
      path = 'foo';
      data = 1;
   }

   // but there's this third library party coupling :(
   if( path !== null ) {
     let ref = admin.database.ref().child(path);
     return ref.set(data)
        .then(() => res.send('done'))
        .catch(e => res.status(500).send(e));
   }
   else {
      res.status(500).send('invalid query');
   }
};

To test this example, I have to include and initialize Functions as well as the Firebase Admin SDK, or I have to find a way to mock those services. All of this looks like a pretty big job. Instead, I can have a DataStore abstraction and utilize that:

// An interface for the DataStore abstraction
// This is where my Firebase logic would go, neatly packaged
// and decoupled
class DataStore {
   set: (path, data) => {
      // This is the home for admin.database.ref(path).set(data);
   }
}

// An interface for the HTTPS abstraction
class ResponseHandler {
   success: (message) => { /* res.send(message); */ }
   fail: (error) => { /* res.status(500).send(error); */ } 
}

If I now add in the first principle of abstracting my logic from the Functions process, then I have a layout like the following:

// functions/lib/http.js
exports.httpFunction = (query, responseHandler, dataStore) => {
   if( query.foo ) {
      return dataStore.set('foo', 1)
        .then(() => responseHandler.success())
        .catch(e => responseHandler.fail(e));
   }
   else {
      responseHandler.fail('invalid query');
   }
};

Allowing me to write a unit test that's much more elegant:

// spec/lib/http
describe('functions/lib/http', () => {
   expect('is successful if "foo" parameter is passed', () => {
      // first thing to do is mock req and res objects
      const query = {foo: 'bar'};
      const responseHandler = {success: () => {}, fail: () => {});
      const dataStore = {set: () => {return Promise.resolve()}};

      // now let's monitor the results
      spyOn(responseHandler, 'success');

      // now run it
      http.httpFunction(query, responseHandler, dataStore);

      // new test it
      expect(res.success).toHaveBeenCalled();
   });
}); 

And the remainder of my code isn't half bad either:

// functions/lib/firebase.datastore.js
// A centralized place for our third party lib!
// Less mocking and e2e testing!
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const serviceAccount = require(env.serviceAccountPath);

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: `https://${env.dbUrl}.firebaseio.com`
});

exports.set = (path, data) => {
  return admin.database.ref(path).set(data);
};

// functions/index.js
const functions = require('firebase-functions');
const dataStore = require('./lib/firebase.datastore');
const ResponseHandler = require('./lib/express.responseHandler');
const env = require('./env');
const http = require('./lib/http');

dataStore.initialize(env);

exports.httpFunction = (req, res) => {
   const handler = new ResponseHandler(res);
   return http.httpFunction(req.query, handler, dataStore);
};

Not to mention that beginning with a good BDD mindset, I've also nicely isolated the components of my project in a modular way that's going to be nice when we find out about all the scope creep in phase 2. :)



来源:https://stackoverflow.com/questions/46229787/testing-cloud-functions-for-firebase-with-jasmine-and-typescript

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!