问题
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