Document how to emulate existing functions/triggers.
larssn opened this issue ยท 11 comments
So while I appreciate the quickstart examples, they seem to be lacking in how you'd go about testing existing functions/triggers.
I'm not sure if that's where firebase-functions-test
comes into play.
So consider this a small request for a bit more docs to get people started.
@larssn thanks for this feedback! I agree with you, it's not obvious how to get started with testing.
Just as an overview:
-
Unit Testing - use the
firebase-functions-test
library to write unit tests against your Cloud Functions as if they were local javascriptfunction
s. This library helps you construct mock inputs/outputs and invoke the handler correctly. However if your function has side effects (writes to the database calls an external API) you will need to handle mocking those pieces yourself. -
Integration Testing - use the Firebase Emulator Suite to actually execute your Cloud Functions, with all their side effects, in a local environment. Rather than calling a function directly you actually perform the triggering event (ex: a write to the database) and observe the function side effects asynchronously. In this situation you treat your function as a sort of black box and just check the state of the world before/after.
I hope that's helpful for you to get started! cc @markarndt who is in charge of our docs.
Thank you, it does give a bit more insight into the underlying idea.
Assuming my index.ts
file looks like this:
import 'source-map-support/register';
import * as admin from 'firebase-admin';
const config: admin.AppOptions = JSON.parse(process.env.FIREBASE_CONFIG || '{}');
Object.assign(config, {
credential: admin.credential.cert(require('../firebase-service-account.json')),
databaseURL: 'https://my-app.firebaseio.com',
});
admin.initializeApp(config);
export const trigger = functions.region('europe-west1').firestore
.document('businesses/{businessId}/products/{productId}')
.onCreate(async (snapshot, context) => {
// ... code to be tested
});
I wish to test that trigger
works without errors. Is that possible with @firebase/testing
?
What I've tried so far, is to simply import trigger
in my test.ts
, but that also seems to execute the normal initializeApp
logic, which I would imagine isn't desirable when using @firebase/testing
.
This is just a small example. Our actual setup is using firebase-admin
in my different files, and they all assume that firebase-admin
is already initialized.
@larssn ah yes I forgot to mention @firebase/testing
which may be the most confusing one of all :-) ... that library is used for mocking out app initialization and auth state when talking to the emulators.
For your situation I would write a test that looks like this. Please excuse any errors I am coding directly in the GitHub box here haha.
The key is to run this test while the Firestore and Functions emulators are running using something like:
$ firebase emulators:exec "npm test"
Mocha test
const testing = require("@firebase/testing");
describe("trigger", () => {
it("does what I expect", async (done) => {
// You could use initializeTestApp if you want to simulate a write from a user (which goes
// through security rules). You need to use your real project ID so that the Functions and
// Firestore emulators can communicate.
const fakeApp = test.initializeAdminApp({
projectId: "YOUR_REAL_PROJECT_ID"
});
const db = fakeApp.firestore();
await db.collection('businesses/foo/products').add({ ... some data ... });
// At this point you need to run a loop or a listener to make sure the function
// does what it's supposed to. The function will excute async after the Firestore write completes.
// For instance, check every 100ms
const intervalId = setInterval(() => {
if (conditionIsMet()) {
clearInterval(intervalId);
done();
}
}, 100);
});
});
Thanks Sam, I appreciate it!
One last question. Does the emulator intercept the init logic in my main index.ts
(admin.initializeApp(config)
). I'd rather not have to worry about my test getting initialized to the production server! :-)
edit
Maybe it's a moot point actually. In your example above, there really isn't any need in importing any functions.
@larssn if you do admin.initializeApp()
with no arguments the emulator will intercept that and point at other emulators (when available). If you do admin.initializeApp({ ... some custom config })
we will follow your config which means we will probably point at production!
It's worth noting that using the empty admin.initializeApp()
almost always does the right thing. In production Cloud Functions it uses the default service account. In the emulator it points to other emulators (when they are running).
Also when running inside the emulator you will see process.env.FUNCTIONS_EMULATOR = true
so you can use that to branch your code if necessary.
I'll scrap the service-account, and see if it still works. It's pretty old code from a few years back, and we're not live with what I'm working on here.
Anyway, I'm jumping for joy with the stellar support, Sam! ๐ ๐ ๐ ๐
@larssn any time! Thanks for your patience. I'll leave this open for docs-improving purposes.
Am I right in assuming that I still need a GOOGLE_APPLICATION_CREDENTIALS
env var?
Without it, my functions can't access some metadata endpoint in GCP.
@larssn most of the time you don't need that but sometimes you do. The reason is that Firebase locally authorizes as a user (you) whereas when in production you authorize as a service account. In the past those two things were basically interchangeable with the right scopes but now GCP is starting to reject user tokens for some services.
What's the actual endpoint you can't access and / or the error you're getting? Also do you know what code is causing it?
@larssn hmmm I'm not sure but would you mind moving your last two comments to a new issue? I'd like this one to remain about docs confusion.
Also when working inside Cloud Functions, initializing without parameters is preferred:
https://firebase.google.com/docs/admin/setup#initialize_without_parameters
@samtstern How to write tests for HTTPS callable function in such a case
I am trying to write integration tests. I trigger the function from the tests, but it just returns with a not-found
or an internal
error