Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document how to emulate existing functions/triggers. #96

Open
larssn opened this issue Sep 30, 2019 · 11 comments
Open

Document how to emulate existing functions/triggers. #96

larssn opened this issue Sep 30, 2019 · 11 comments
Assignees

Comments

@larssn
Copy link

larssn commented Sep 30, 2019

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.

@samtstern
Copy link
Contributor

@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 javascript functions. 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.

@larssn
Copy link
Author

larssn commented Sep 30, 2019

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.

@samtstern
Copy link
Contributor

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

@larssn
Copy link
Author

larssn commented Sep 30, 2019

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.

@samtstern
Copy link
Contributor

@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.

@larssn
Copy link
Author

larssn commented Sep 30, 2019

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! 👍 👍 👍 😄

@samtstern
Copy link
Contributor

@larssn any time! Thanks for your patience. I'll leave this open for docs-improving purposes.

@larssn
Copy link
Author

larssn commented Sep 30, 2019

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.

@samtstern
Copy link
Contributor

@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?

@samtstern
Copy link
Contributor

@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

@mohdabbas378
Copy link

mohdabbas378 commented Jun 26, 2020

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants