Unit Testing of Cloud Functions

Sometimes you may want to test your functions locally, without deploying. This can be useful for unit testing or for debugging during development. This page describes recommended best practices for local unit testing.

Invoking functions locally

To invoke your functions locally, create a file named testing.js in the /functions/test/ directory and import index.js as a module:

var myFunctions = require('../index');

Each of your functions is exported as a property on the required module, and it is just a JavaScript function. So if you had a function named "doSomething", you could call it with myFunctions.doSomething().

This page illustrates how to create unit tests by using the test framework Mocha for two sample functions from the Get Started tutorial: the HTTPS function addMessage() and the Realtime Database function makeUppercase().

Invoking non-HTTPS functions

All functions, except for HTTPS functions, take an Event object as a parameter. An Event is a JavaScript object containing all of the information about the triggered event, including any data payload. The data property for the event looks different, depending on the type of trigger. For database functions like makeUppercase(), the data property is a DeltaSnapshot, which needs to be constructed in the fake event. To stub a database write event for the makeUppercase() function, the code might look like this:

const fakeEvent = {
  // The DeltaSnapshot constructor is used by the Functions SDK to transform a raw event from
  // your database into an object with utility functions such as .val().
  // Its signature is: DeltaSnapshot(app: firebase.app.App, adminApp: firebase.app.App,
  // data: any, delta: any, path?: string);
  // We can pass null for the first 2 parameters. The data parameter represents the state of
  // the database item before the event, while the delta parameter represents the change that
  // occured to cause the event to fire. The last parameter is the database path, which we are
  // not making use of in this test. So we will omit it.
  data: new functions.database.DeltaSnapshot(null, null, null, 'input'),
  // To mock a database delete event:
  // data: new functions.database.DeltaSnapshot(null, null, 'old_data', null)
};

To invoke makeUppercase() with this stub event, do the following:

myFunctions.makeUppercase(fakeEvent);

In Google Cloud Functions, all non-HTTPS functions return a promise, even if your code does not. Upon successful completion, the promise resolves with the return value of your function. You can make assertions about the expected return value using chai-as-promised.

return assert.eventually.equal(myFunctions.makeUppercase(fakeEvent), expectedVal);

If you try to invoke makeUppercase() at this point, you get an error message. This is because makeUppercase() tried to write to the Firebase Realtime Database. If your function makes any writes to the database, you can mock a database write.

Invoking HTTPS functions

An HTTPS-triggered function takes two parameters: a request object and a response object. Here is how you might test the addMessage() example function: Override the redirect function in the response object, since sendMessage() calls it. Within the redirect function, use chai.assert to help make assertions about what parameters the redirect function should be called with:

// A fake request object, with req.query.text set to 'input'
const req = { query: {text: 'input'} };
// A fake response object, with a stubbed redirect function which asserts that it is called
// with parameters 303, 'new_ref'.
const res = {
  redirect: (code, url) => {
    assert.equal(code, 303);
    assert.equal(url, 'new_ref');
    done();
  }
};

// Invoke addMessage with our fake request and response objects. This will cause the
// assertions in the response object to be evaluated.
myFunctions.addMessage(req, res);

If you try to run the above code, you get an error message. This is because sendMessage() tried to write to the Firebase Realtime Database. If your function makes any writes to the database, you can mock a database write.

All HTTPS-triggered functions are also Express apps, with the same request and response object types, so you can make use of libraries like Supertest and MockExpress in your testing as well.

Mocking functions.config() and admin.initializeApp()

If your code uses Runtime Config values, it is necessary to mock functions.config() to test your functions locally.

In the source code for makeUppercase() and sendMessage(), note these lines at the top of the file:

// index.js
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

To avoid getting an error from initializeApp(), you must stub the Firebase config values as well as admin.initializeApp(). A great place to do this is in the "before" function of your Mocha tests, which gets executed before any tests are run. You can then restore these stubs in the "after" function.

In test.js:

var myFunctions, configStub, adminInitStub, functions, admin;

before(() => {
  // Since index.js makes calls to functions.config and admin.initializeApp at the top of the file,
  // we need to stub both of these functions before requiring index.js. This is because the
  // functions will be executed as a part of the require process.
  // Here we stub admin.initializeApp to be a dummy function that doesn't do anything.
  admin =  require('firebase-admin');
  adminInitStub = sinon.stub(admin, 'initializeApp');
  // Next we stub functions.config(). Normally config values are loaded from Cloud Runtime Config;
  // here we'll just provide some fake values for firebase.databaseURL and firebase.storageBucket
  // so that an error is not thrown during admin.initializeApp's parameter check
  functions = require('firebase-functions');
  configStub = sinon.stub(functions, 'config').returns({
      firebase: {
        databaseURL: 'https://not-a-project.firebaseio.com',
        storageBucket: 'not-a-project.appspot.com',
      }
      // You can stub any other config values needed by your functions here, for example:
      // foo: 'bar'
    });
  // Now we can require index.js and save the exports inside a namespace called myFunctions.
  // This includes our cloud functions, which can now be accessed at myFunctions.makeUppercase
  // and myFunctions.addMessage
  myFunctions = require('../index');
});

after(() => {
  // Restoring our stubs to the original methods.
  configStub.restore();
  adminInitStub.restore();
});

Mocking database writes

If you mock functions.config() with a test app's real Firebase configuration values, any writes to the database that your function makes actually get executed against the real database. This may be useful for debugging. However, if you would like to write unit tests that are isolated and have no side effects, you should mock database writes.

Mocking writes to event.data.ref()

To get started mocking database writes, it's useful to analyze a function's code and ensure that you mock all the necessary components. For example, to analyze makeUppercase(), do the following:

return event.data.ref.parent.child('uppercase').set(uppercase);

The above line is a series of the following properties and functions:

  • event: you need to fake an Event object, which was covered in Invoking non-HTTPS functions.
  • data: the fake event needs to have a data field.
  • ref: this is a getter() function, so it needs to be stubbed with refStub().
  • parent: this is a property, not a function, so you need to ensure that refStub() returns an object that has a parent property.
  • child: this is a function that will be stubbed with childStub(). Sinon has the ability to specify that a function should only be stubbed if it is called with particular arguments. In this case, you want to only stub the child() function if it is called with uppercase() as a parameter.
  • set: this is a function that will be stubbed with setStub(). The function is expected to be called with the capitalized word INPUT. Because this is the last function in the chain, you can set a return value for the stub function, and then check for this later to verify that the function has executed correctly.
const childParam = 'uppercase';
const setParam = 'INPUT';
// Stubs are objects that fake and/or record function calls.
// These are excellent for verifying that functions have been called and to validate the
// parameters passed to those functions.
const refStub = sinon.stub();
const childStub = sinon.stub();
const setStub = sinon.stub();
// The following 4 lines override the behavior of event.data.ref.parent.child('uppercase')
// .set('INPUT') to return true
Object.defineProperty(fakeEvent.data, 'ref', { get: refStub });
refStub.returns({ parent: { child: childStub}});
childStub.withArgs(childParam).returns( { set: setStub });
setStub.withArgs(setParam).returns(true);

Since the final function, set(), was stubbed to return true, you can check for this return value as a way to verify that this whole chain of stubs was called properly.

// All non-HTTPS cloud functions return a promise that resolves with the return value of your
// code. In this case, we've stubbed it to return true if
// event.data.ref.parent.child(childParam).set(setParam) was called with the parameters we
// expect. We assert that makeUppercase returns a promise that eventually resolves with true.
return assert.eventually.equal(myFunctions.makeUppercase(fakeEvent), true);

Mocking writes to the admin app's database

Mocking writes to the admin app's database is very similar to mocking writes to event.data.ref(). First, examine how the function code does the write, then create stubs for each function call in the chain.

This is how addMessage() is doing the database write:

admin.database().ref('/messages').push({original: original}).then(snapshot => { res.redirect(303, snapshot.ref); });

Notice here that push() is supposed to return a promise, which resolves with a snapshot value that has a ref() property. It's important to ensure that the stub returns this so that res.direct() can work properly.

const refParam = '/messages';
const pushParam = { original: 'input' };
const refStub = sinon.stub();
const pushStub = sinon.stub();

// The following 4 lines override the behavior of admin.database().ref('/messages')
// .push({ original: 'input' }) to return a promise that resolves with { ref: 'new_ref' }.
// This mimics the behavior of a push to the database, which returns an object containing a
// ref property representing the URL of the newly pushed item.
databaseStub = sinon.stub(admin, 'database');
databaseStub.returns( { ref: refStub });
refStub.withArgs(refParam).returns( { push: pushStub });
pushStub.withArgs(pushParam).returns( Promise.resolve({ ref: 'new_ref' }));

Review complete sample test code

You can view the complete unit tests for makeUppercase() and sendMessage() as well as the original functions on our GitHub repository.

Send feedback about...

Need help? Visit our support page.