firebase / firebase-functions-test

MIT License
232 stars 48 forks source link

Race condition: Unit testing (Lazy functions) #38

Closed mwalkerwells closed 4 years ago

mwalkerwells commented 5 years ago

Problem

Since functions are lazily loaded, calling test.firestore.exampleDocumentSnapshot (and others) after stubbing initializeApp & importing your cloud functions file results in an error...

Error: The default Firebase app does not exist. Make sure you call initializeApp() before using any of the Firebase services.

Solution?

Add a test.setup() (pairs well with test.cleanUp()) function that calls src/app.ts#L42-L51 (attached below).

If I'm missing something, please let me know! Following the firestore documentation for unit tests led to some really unexpected behavior & was surprised to discover this (a lot of time)...

Another example here: https://stackoverflow.com/questions/49661782/firebase-functions-test-the-default-firebase-app-does-not-exist


Example Test (with workaround)

test.js

// IMPORTS
const chai = require('chai');
const assert = chai.assert;
const sinon = require('sinon');
const admin = require('firebase-admin');
const test = require('firebase-functions-test')();

// TESTS
describe('Cloud Functions', () => {
  let myFunctions, adminInitStub;

  before(() => {
    // ORDER REQUIRED
    // 1) Calling 'exampleDocumentSnapshot' only way to initialize testing app
    test.firestore.exampleDocumentSnapshot()

    // 2) Prevents multiple initializeApp calls
    adminInitStub = sinon.stub(admin, 'initializeApp');

    // 3) Import functions
    myFunctions = require('../');
  });

  after(() => { adminInitStub.restore(); test.cleanup(); });

  describe('Firestore Events', () => {

    it('', () => {
      const snapshotBefore  = test.firestore.exampleDocumentSnapshot();
      const snapshotAfter   = test.firestore.exampleDocumentSnapshot();
      const snapshotChange  = test.makeChange(snapshotBefore, snapshotAfter);
      const updateStub      = sinon.stub();
      const wrapped         = test.wrap(myFunctions. my_firestore_cloud_function);

      snapshotChange.after.ref.update = updateStub;
      updateStub.withArgs(snapshotChange.after.data()).returns(true);

      return assert.equal(wrapped(snapshotChange), true);

    })
  });

})

../index.js

// SIDE EFFECTS
const admin = require('firebase-admin');
admin.initializeApp();

// IMPORTS
// Firebase
const functions = require('firebase-functions');

// my_firestore_cloud_function :: CloudFunction Change DocumentSnapshot
const my_firestore_cloud_function = functions.firestore.document('/<collection>/<document>/').onWrite((change, context) => {
  // DO THINGS
});

Firebase Functions Test Library

firebase-functions-test/lib/providers/firestore.js

firestoreService = firestore(testApp().getApp());

https://github.com/firebase/firebase-functions-test/blob/master/src/providers/firestore.ts#L60

firebase-functions-test/lib/app.js

    getApp(): firebase.app.App {
      if (typeof this.appSingleton === 'undefined') {
        this.appSingleton = firebase.initializeApp(
          JSON.parse(process.env.FIREBASE_CONFIG),
          // Give this app a name so it does not conflict with apps that user initialized.
          'firebase-functions-test',
        );
      }
      return this.appSingleton;
    }

https://github.com/firebase/firebase-functions-test/blob/master/src/app.ts#L42-L51

mwalkerwells commented 5 years ago

package.json

//...
    "firebase-admin": "^6.0.0",
    "firebase-functions": "^2.1.0",
    "@types/mocha": "^5.2.5",
    "chai": "^4.2.0",
    "firebase-functions-test": "^0.1.6",
//...
silenceisgolden commented 5 years ago

This is an excellent report and based off of the numerous number of reports for this issue, it would be in everyone's best interest if we could get an update on what the actual current best practice is. Right now it seems like no matter how you try to set up the test and the initializeApp() step, this error is what you get.

laurenzlong commented 4 years ago

Thanks for the report and I'm very sorry for the long silence on this! What you're trying to do is actually unsupported behavior, when you initialize the test SDK in offline mode you should be using stubbed data instead of calling exampleDocumentSnapshot or makeChange, see https://firebase.google.com/docs/functions/unit-testing#using_stubbed_data_for_offline_mode. When you have not initialize the test SDK in online mode, trying to do anything that requires a real Firebase app (which constructing a snapshot requires) leads to unexpected behavior. I will work with our tech writers to make this more clear in documentation.

astyltsvig commented 3 years ago

Thanks for the report and I'm very sorry for the long silence on this! What you're trying to do is actually unsupported behavior, when you initialize the test SDK in offline mode you should be using stubbed data instead of calling exampleDocumentSnapshot or makeChange, see https://firebase.google.com/docs/functions/unit-testing#using_stubbed_data_for_offline_mode. When you have not initialize the test SDK in online mode, trying to do anything that requires a real Firebase app (which constructing a snapshot requires) leads to unexpected behavior. I will work with our tech writers to make this more clear in documentation.

Hello!

I feel that what you are saying, and what their documentation states are in contradictory with each other.

image

The documentation says that we are perfectly able to use makeChange on a offline-mode testing?