I successfully wrote some tests for my Firebase functions, however now I want to test functions that manipulate Firestore data.
to do so I execute the following
export GOOGLE_APPLICATION_CREDENTIALS="my-project-key.json"
export FIRESTORE_EMULATOR_HOST="localhost:8080"
firebase emulators:start --import ./functions/test/fixture --project my-project
and then I run
npm run test
The test code is as follows:
const test = require("firebase-functions-test")()
const functions = require("../index")
describe("Tests", () => {
it("Do test", async () => {
const wrapped = test.wrap(functions.doTest)
const result = await wrapped({id:"1"})
})
})
The index file imported contains:
const functions = require("firebase-functions")
const admin = require("firebase-admin")
admin.initializeApp()
exports.doTest = functions.https.onCall(async (data) => {
const {id} = data
const vehicleRef = admin.firestore().collection("vehicles").doc(id)
const vehicle = await vehicleRef.get()
})
Yet every time I call a Firebase function that accesses Firestore I get the following error (in this case on vehicleRef.get()):
Error: Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.
at GoogleAuth.getApplicationDefaultAsync (/MyProject/firebase/functions/node_modules/google-auth-library/build/src/auth/googleauth.js:157:19)
at processTicksAndRejections (internal/process/task_queues.js:94:5)
at async GoogleAuth.getClient (/MyProject/firebase/functions/node_modules/google-auth-library/build/src/auth/googleauth.js:490:17)
at async GrpcClient._getCredentials (/MyProject/firebase/functions/node_modules/google-gax/build/src/grpc.js:87:24)
at async GrpcClient.createStub (/MyProject/firebase/functions/node_modules/google-gax/build/src/grpc.js:212:23)
Caused by: Error
at Firestore.getAll (/MyProject/firebase/functions/node_modules/#google-cloud/firestore/build/src/index.js:784:23)
at DocumentReference.get (/MyProject/firebase/functions/node_modules/#google-cloud/firestore/build/src/reference.js:201:32)
at Function.run (/MyProject/firebase/functions/src/tracker.js:40:28)
at wrapped (/MyProject/firebase/functions/node_modules/firebase-functions-test/lib/main.js:72:30)
at Context.<anonymous> (/MyProject/firebase/functions/test/testTracker.js:42:26)
at callFn (/MyProject/firebase/functions/node_modules/mocha/lib/runnable.js:366:21)
at Test.Runnable.run (/MyProject/firebase/functions/node_modules/mocha/lib/runnable.js:354:5)
at Runner.runTest (/MyProject/firebase/functions/node_modules/mocha/lib/runner.js:677:10)
at /MyProject/firebase/functions/node_modules/mocha/lib/runner.js:801:12
at next (/MyProject/firebase/functions/node_modules/mocha/lib/runner.js:594:14)
at /MyProject/firebase/functions/node_modules/mocha/lib/runner.js:604:7
at next (/MyProject/firebase/functions/node_modules/mocha/lib/runner.js:486:14)
at Immediate._onImmediate (/MyProject/firebase/functions/node_modules/mocha/lib/runner.js:572:5)
What am I doing wrong?
Looks like you're doing everything right, except I don't think you're initializing the initializeApp object correctly. Try this in your index:
admin.initializeApp({
projectId: 'my-project',
credential: admin.credential.applicationDefault(),
});
You'll have to add a condition to use this version based on whether or not the environment variable is set though.
Related
I am trying to deploy some cloud functions using algolia search but it gives me some errors.
When deleting this line:
const algoliasearch = require('algoliasearch') and functions that related to algolia, it deploys successfully.
The error is:
=== Deploying to 'ecommerce-8e525'...
i deploying functions
+ functions: Finished running predeploy script.
i functions: ensuring required API cloudfunctions.googleapis.com is enabled...
i functions: ensuring required API cloudbuild.googleapis.com is enabled...
+ functions: required API cloudbuild.googleapis.com is enabled
+ functions: required API cloudfunctions.googleapis.com is enabled
i functions: preparing functions directory for uploading...
i functions: packaged functions (2.92 KB) for uploading
+ functions: functions folder uploaded successfully
The following functions are found in your project but do not exist in your local source code:
onCreateSection(us-central1)
onDeleteCategory(us-central1)
onDeleteSection(us-central1)
onUpdateCategory(us-central1)
onUpdateSection(us-central1)
If you are renaming a function or changing its region, it is recommended that you create the new function first before deleting the old one to prevent event loss.
For more info, visit https://firebase.google.com/docs/functions/manage-functions#modify
? Would you like to proceed with deletion? Selecting no will continue the rest of the deployments. No
i functions: creating Node.js 14 function onCreateSubCategory(us-central1)...
i functions: creating Node.js 14 function onUpdateSubCategory(us-central1)...
i functions: creating Node.js 14 function onDeleteSubCategory(us-central1)...
i functions: creating Node.js 14 function onUpdateMainCategory(us-central1)...
i functions: creating Node.js 14 function onDeleteMainCategory(us-central1)...
i functions: updating Node.js 14 function onDeleteProduct(us-central1)...
i functions: updating Node.js 14 function onCreateCustomer(us-central1)...
i functions: updating Node.js 14 function onUpdateCustomer(us-central1)...
i functions: updating Node.js 14 function onDeleteCustomer(us-central1)...
i functions: cleaning up build files...
Functions deploy had errors with the following functions:
onCreateSubCategory(us-central1)
onDeleteCustomer(us-central1)
onCreateCustomer(us-central1)
onUpdateSubCategory(us-central1)
onUpdateCustomer(us-central1)
onUpdateMainCategory(us-central1)
onDeleteProduct(us-central1)
onDeleteSubCategory(us-central1)
onDeleteMainCategory(us-central1)
To try redeploying those functions, run:
firebase deploy --only "functions:onCreateSubCategory,functions:onDeleteCustomer,functions:onCreateCustomer,functions:onUpdateSubCategory,functions:onUpdateCustomer,functions:onUpd
ateMainCategory,functions:onDeleteProduct,functions:onDeleteSubCategory,functions:onDeleteMainCategory"
To continue deploying other features (such as database), run:
firebase deploy --except functions
Error: Functions did not deploy properly.
This is my code:
const functions = require("firebase-functions");
const admin = require('firebase-admin');
const algoliasearch = require('algoliasearch');
const ALGOLIA_APP_ID = "APP_ID";
const ALGOLIA_ADMIN_KEY = "ADMIN_KEY";
const ALGOLIA_INDEX_NAME = "users";
admin.initializeApp();
const storage = admin.storage();
// When admin create sub category:
exports.onCreateSubCategory = functions.firestore
.document("/subCategories/{subCategoryId}")
.onCreate(async (snapshot, context) => {
const createdSubCategory = snapshot.data();
const subCategoryId = context.params.subCategoryId;
const mainCategoryId = createdSubCategory['main_category_id'];
// add it to main category
admin
.firestore()
.collection("mainCategories")
.doc(mainCategoryId).update({
'sub_categories_ids': admin.firestore.FieldValue.arrayUnion(subCategoryId)
});
});
// When admin update sub category:
exports.onUpdateSubCategory = functions.firestore.document("/subCategories/{subCategoryId}").onUpdate(async (change, context) => {
const updatedSubCategoryData = change.after.data();
const oldSubCategoryData = change.before.data();
const productsRef = admin.firestore().collection('products');
// case of updating image
// delete old image from storage
var oldImage = oldSubCategoryData['image_id'];
const bucket = storage.bucket();
const path = "subCategories/" + oldImage;
await bucket.file(path).delete();
// case of updating name
// update sub category name for related products
const productsQuerySnapshot = await productsRef.where('sub_category', '==', oldSubCategoryData['name']).get();
productsQuerySnapshot.forEach(doc => {
productsRef.doc(doc.id).update({
"sub_category": updatedSubCategoryData['name'],
"categories": admin.firestore.FieldValue.arrayRemove(oldSubCategoryData['name']),
"categories": admin.firestore.FieldValue.arrayUnion(updatedSubCategoryData['name']),
})
})
})
// When admin delete sub category:
exports.onDeleteSubCategory = functions.firestore.document("/subCategories/{subCategoryId}").onDelete(async (snapshot, context) => {
const subCategoryData = snapshot.data();
const imageId = subCategoryData['image_id'];
console.log('image_id is ',imageId);
// 1- delete sub category image from storage
const bucket = storage.bucket();
const path = "subCategories/" + imageId;
await bucket.file(path).delete();
// 2- delete sub category from products
const productsRef = admin.firestore().collection('products');
const productsQuerySnapshot = await productsRef.where('sub_category', '==', subCategoryData['name']).get();
productsQuerySnapshot.forEach( async doc => {
await productsRef.doc(doc.id).update({
'sub_category': {},
'categories': admin.firestore.FieldValue.arrayRemove(subCategoryData['name'])
})
})
})
//4) when admin update main category
exports.onUpdateMainCategory = functions.firestore.document("/mainCategories/{mainCategoryId}").onUpdate(async (change, context) => {
const updatedMainCategoryData = change.after.data();
const oldMainCategoryData = change.before.data();
const mainCategoryId = context.params.mainCategoryId;
//1- when admin change main category name
/// change main category name for related products
const productsRef = admin.firestore().collection('products');
const productsQuerySnapshot = await productsRef.where('categories', 'array-contains', oldMainCategoryData['name']).get();
productsQuerySnapshot.forEach( async doc =>{
await productsRef.doc(doc.id).update({
'categories': FieldValue.arrayRemove(oldMainCategoryData['name']),
'categories': admin.firestore.FieldValue.arrayUnion(updatedMainCategoryData['name'])
})
})
//2- when admin remove subCategory
/// delete them from subCategories collection
const oldSubCategoriesIds = oldMainCategoryData['sub_categories_ids'];
const updatedSubCategories = updatedMainCategoryData['sub_categories_ids'];
let difference = oldSubCategoriesIds.filter(x => !updatedSubCategories.includes(x));
difference.forEach(async id =>{
await admin.firestore().collection('subCategories').doc(id).delete();
})
})
// 5) when admin delete main category
exports.onDeleteMainCategory = functions.firestore.document("/mainCategories/{mainCategoryId}").onDelete(async (snapshot, context) => {
const mainCategoryData = snapshot.data();
const subCategoriesIds = mainCategoryData['sub_categories_ids'];
// delete category from products
const productsRef = admin.firestore().collection('products');
const productsQuerySnapshot = await productsRef.where('categories', 'array-contains', mainCategoryData['name']).get();
productsQuerySnapshot.forEach(async doc =>{
await productsRef.doc(doc.id).update({
'categories': admin.firestore.FieldValue.arrayRemove(mainCategoryData['name'])
})
})
// delete sub categories
subCategoriesIds.forEach(async id =>{
await admin.firestore().collection('subCategories').doc(id).delete();
})
})
// 6) when admin delete product
exports.onDeleteProduct = functions.firestore.document("/products/{productId}").onDelete(async (snapshot, context) => {
const categoryData = snapshot.data();
const imageId = categoryData['image_id'];
// delete it's image from storage
const bucket = storage.bucket();
const path = "products/" + imageId;
return bucket.file(path).delete();
})
// 7) onCreate Customer
exports.onCreateCustomer = functions.firestore
.document('users/{userId}')
.onCreate( async (snap, context) => {
const newValue = snap.data();
newValue.objectID = snap.id;
var client = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_ADMIN_KEY);
var index = client.initIndex(ALGOLIA_INDEX_NAME);
index.saveObject(newValue);
console.log("Finished");
});
// 8) onUpdate Customer
exports.onUpdateCustomer = functions.firestore
.document('users/{userId}')
.onUpdate( async (snap, context) => {
const afterUpdate = snap.after.data();
afterUpdate.objectID = snap.after.id;
var client = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_ADMIN_KEY);
var index = client.initIndex(ALGOLIA_INDEX_NAME);
index.saveObject(afterUpdate);
});
// 9) onDelete Customer
exports.onDeleteCustomer = functions.firestore
.document('users/{userId}')
.onDelete( async (snap, context) => {
const oldID = snap.id;
var client = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_ADMIN_KEY);
var index = client.initIndex(ALGOLIA_INDEX_NAME);
index.deleteObject(oldID);
});
Edit I tried to run with "firebase deploy --only function --debug and this is the result "
Functions deploy had errors with the following functions: onCreateSubCategory(us-central1) onDeleteCustomer(us-central1) onDeleteProduct(us-central1) onCreateCustomer(us-central1) onDeleteMainCategory(us-central1) onUpdateMainCategory(us-central1) onUpdateCustomer(us-central1) onDeleteSubCategory(us-central1) onUpdateSubCategory(us-central1) To try redeploying those functions, run: firebase deploy --only "functions:onCreateSubCategory,functions:onDeleteCustomer,functions:onDeleteProduct,functions:onCreateCustomer,functions:onDeleteMainCategory,functions:onUpd ateMainCategory,functions:onUpdateCustomer,functions:onDeleteSubCategory,functions:onUpdateSubCategory" To try redeploying those functions, run: firebase deploy --only "functions:onCreateSubCategory,functions:onDeleteCustomer,functions:onDeleteProduct,functions:onCreateCustomer,functions:onDeleteMainCategory,functions:onUpdateMainCategory,functions: onUpdateCustomer,functions:onDeleteSubCategory,functions:onUpdateSubCategory"
To continue deploying other features (such as database), run: firebase deploy --except functions [2021-07-31T19:29:21.057Z] Error during update for projects/ecommerce-8e525/locations/us-central1/functions/onCreateSubCategory: firebase deploy --only "functions:onCreateSubCategory,functions:onDeleteCustomer,functions:onDeleteProduct,functions:onCreateCustomer,functions:onDeleteMainCategory,functions:onUpdateMainCategory,functions: onUpdateCustomer,functions:onDeleteSubCategory,functions:onUpdateSubCategory" To continue deploying other features (such as database), run: firebase deploy --except functions [2021-07-31T19:29:21.057Z] Error during update for projects/ecommerce-8e525/locations/us-central1/functions/onCreateSubCategory: [2021-07-31T19:29:21.058Z] Error during update for projects/ecommerce-8e525/locations/us-central1/functions/onDeleteCustomer: [2021-07-31T19:29:21.058Z] Error during update for projects/ecommerce-8e525/locations/us-central1/functions/onDeleteProduct: [2021-07-31T19:29:21.058Z] Error during update for projects/ecommerce-8e525/locations/us-central1/functions/onCreateCustomer: [2021-07-31T19:29:21.058Z] Error during update for projects/ecommerce-8e525/locations/us-central1/functions/onDeleteMainCategory: [2021-07-31T19:29:21.058Z] Error during update for projects/ecommerce-8e525/locations/us-central1/functions/onUpdateMainCategory: [2021-07-31T19:29:21.058Z] Error during update for projects/ecommerce-8e525/locations/us-central1/functions/onUpdateCustomer: firebase deploy --only "functions:onCreateSubCategory,functions:onDeleteCustomer,functions:onDeleteProduct,functions:onCreateCustomer,functions:onDeleteMainCategory,functions:onUpd ateMainCategory,functions:onUpdateCustomer,functions:onDeleteSubCategory,functions:onUpdateSubCategory"
To continue deploying other features (such as database), run: firebase deploy --except functions [2021-07-31T19:29:21.057Z] Error during update for projects/ecommerce-8e525/locations/us-central1/functions/onCreateSubCategory: [2021-07-31T19:29:21.058Z] Error during update for projects/ecommerce-8e525/locations/us-central1/functions/onDeleteCustomer: [2021-07-31T19:29:21.058Z] Error during update for projects/ecommerce-8e525/locations/us-central1/functions/onDeleteProduct: [2021-07-31T19:29:21.058Z] Error during update for projects/ecommerce-8e525/locations/us-central1/functions/onCreateCustomer: [2021-07-31T19:29:21.058Z] Error during update for projects/ecommerce-8e525/locations/us-central1/functions/onDeleteMainCategory: [2021-07-31T19:29:21.058Z] Error during update for projects/ecommerce-8e525/locations/us-central1/functions/onUpdateMainCategory: [2021-07-31T19:29:21.058Z] Error during update for projects/ecommerce-8e525/locations/us-central1/functions/onUpdateCustomer: [2021-07-31T19:29:21.059Z] Error during update for projects/ecommerce-8e525/locations/us-central1/functions/onDeleteSubCategory: [2021-07-31T19:29:21.059Z] Error during update for projects/ecommerce-8e525/locations/us-central1/functions/onUpdateSubCategory:
Error: Functions did not deploy properly.
Remove these installations and reinstall again solves the problem .
npm install -g firebase-tools
firebase login
firebase init
cd functions
npm install algoliasearch --save
firebase functions:config:set algolia.appid="YOUR_APP_ID" algolia.apikey="YOUR_API_KEY"
I have been trying to get firebase functions to work for a day but failing miserably.
I have set up a http cloud function from the google cloud functions inline editor with code:
const admin = require('firebase-admin');
const functions = require('firebase-functions')
admin.initializeApp();
const db = admin.firestore();
exports.account_create_callable = functions.https.onCall((data,context) => {
var docRef = db.collection('users').doc(context.auth.uid)
docRef.set(
{
createdAt:Date.now(),
}, { merge: true }
);
return
})
I'm using React and there is very limited documentation but I've put together what I think is correct:
import firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";
import "firebase/functions";
const firebase = firebase.initializeApp({
apiKey: process.env.REACT_APP_FB_API_KEY,
authDomain: process.env.REACT_APP_FB_AUTH_DOMAIN,
projectId: process.env.REACT_APP_FB_PROJECT_ID,
});
const firestore = firebase.firestore();
// require('firebase-functions'); // I saw this is some documentation but don't think it's needed?
const functions = firebase.functions()
export function createUser(uid, data) {
const account_create_callable = functions().httpsCallable('account_create_callable')
account_create_callable()
}
However when I run this code, I get an internal error shown in the browser console.
Error: internal
at new HttpsErrorImpl (error.ts:65)
at _errorForResponse (error.ts:175)
at Service.<anonymous> (service.ts:276)
at step (tslib.es6.js:100)
at Object.next (tslib.es6.js:81)
at fulfilled (tslib.es6.js:71)
Interestingly, I get the same error when I call a non existent function like so:
functions().httpsCallable('asdf')
Would appreciate if someone could let me know where I'm going wrong.
My Firebase SDK is up to date.
I set up the function in the inline editor and set my region to eu-west 2, would this have any effect on my client code?
Since your Callable Function is deployed on europe-west2, you must initialize your client SDK with the appropriate region:
var functions = firebase.app().functions('europe-west2');
See https://firebase.google.com/docs/functions/locations#client-side_location_selection_for_callable_functions.
I have the following function in my index.ts file:
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp(functions.config().firebase);
const db = admin.firestore();
const fcm = admin.messaging();
export const sendToDevice = functions.firestore
.document('orders/{orderId}')
.onCreate(async snapshot => {
print("aa")
console.log("osakosak");
const order = snapshot.data();
const querySnapshot = await db
.collection('users')
.doc(order.ustaID)
.collection('tokens')
.get();
const tokens = querySnapshot.docs.map(snap => snap.id);
const payload: admin.messaging.MessagingPayload = {
notification: {
title: 'New Order!',
body: `you sold a ${order.day} for ${order.time}`,
click_action: 'FLUTTER_NOTIFICATION_CLICK'
}
};
return fcm.sendToDevice(tokens, payload);
});
However, when the new document gets added into the order collection, this doesn't get triggered. Even the print and console.log don't work. I tried putting print and console log before export, and it still didn't fire.
Based on your comments ("It depends on cloud_firestore in pubspec.yaml"), it seems that you didn't deploy your Cloud Function correctly.
As a matter of fact, Cloud Functions are totally independent from your Flutter app (your front-end). It is a back-end service. You should deploy it with the Firebase CLI, see the doc. Note that the code shall be in the Firebase Project, not in your Flutter project.
I am developing a firebase cloud function that writes to a firestore database.
During development I want the function to write to a local database. So I've started a firestore emulator. But the data is still written to the actual database.
How can I configure the cloud functions to use the local database?
This is my setup:
import * as functions from 'firebase-functions';
import * as cors from "cors";
import * as admin from "firebase-admin";
const REGION = "europe-west1";
const COLLECTION_CONTACT_FORM = "contact_form";
const serviceAccount = require("../keys/auth-key.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
const corsMiddleware = cors({origin: true});
export const sendContactForm = functions.region(REGION).https.onRequest((request, response) => corsMiddleware(request, response, async () => {
let {text} = request.body;
let result = await admin.firestore().collection(COLLECTION_CONTACT_FORM).add({text});
response.send((result.id));
}));
This is the console output when starting the emulator:
[1] i firestore: Serving WebChannel traffic on at http://localhost:8081
[1] i firestore: Emulator logging to firestore-debug.log
[1] ✔ functions: Emulator started at http://localhost:5000
[1] ✔ firestore: Emulator started at http://localhost:8080
[1] i functions: Watching "path/functions" for Cloud Functions...
[1] ⚠ functions: Your GOOGLE_APPLICATION_CREDENTIALS environment variable points to path/keys/auth-key.json. Non-emulated services will access production using these credentials. Be careful!
[1] ✔ functions[sendContactForm]: http function initialized (http://localhost:5000/project/europe-west1/sendContactForm).
When triggering the local endpoint, the production database is written to.
The firestore admin initializeApp() will correctly handle switching between local emulator and production database depending on where it is running. So if you simply remove the service account credentials it should work properly:
import * as functions from 'firebase-functions';
import * as cors from "cors";
import * as admin from "firebase-admin";
const REGION = "europe-west1";
const COLLECTION_CONTACT_FORM = "contact_form";
admin.initializeApp();
const corsMiddleware = cors({origin: true});
export const sendContactForm = functions.region(REGION).https.onRequest((request, response) => corsMiddleware(request, response, async () => {
let {text} = request.body;
let result = await admin.firestore().collection(COLLECTION_CONTACT_FORM).add({text});
response.send((result.id));
}));
But if for some reason you're trying to write to a firestore database outside of the one that the project is created in, you can use firestore/grpc separately from the firebase classes and then use the environment to either include your service account credentials or location emulator credentials. A local emulator example:
const {Firestore} = require('#google-cloud/firestore');
const {credentials} = require('#grpc/grpc-js');
const db = new Firestore({
projectId: 'my-project-id',
servicePath: 'localhost',
port: 5100,
sslCreds: credentials.createInsecure(),
customHeaders: {
"Authorization": "Bearer owner"
}
});
await db.collection("mycollection").doc("someid").set({ test: "value" });
Same answer, but with the docId set dynamically.
exports.makeUppercase = functions.firestore.document('Messages/{docId}').onCreate((snap, context) => {
const original = snap.data().original;
functions.logger.log('Uppercasing', context.params.docId, original);
const uppercase = original.toUpperCase();
// return snap.ref.set({ uppercase }, { merge: true });
return admin.firestore().collection('AnotherCollection').doc(context.params.docId).set({ uppercase }, { merge: true });
});
This grabs the docId that was set dynamically and uses it to write to a document with the same name but in a different collection.
Also I left in commented code for writing to the same document in the same collection. Beware that using onUpdate or onWrite instead of onCreate makes an infinite loop as each write triggers the function again!
Currently we are using 'firebase-functions-test' in online mode to test our firebase functions (as described here https://firebase.google.com/docs/functions/unit-testing), which we setup like so:
//setupTests.ts
import * as admin from 'firebase-admin';
const serviceAccount = require('./../test-service-account.json');
export const testEnv = require('firebase-functions-test')({
projectId: 'projectId',
credential: admin.credential.cert(serviceAccount),
storageBucket: 'projectId.appspot.com'
});
const testConfig = {
dropbox: {
token: 'dropboxToken',
working_dir: 'someFolder'
}
};
testEnv.mockConfig(testConfig);
// ensure default firebase app exists:
try {
admin.initializeApp();
} catch (e) {}
We would like to move away from testing against an actual firestore instance in our tests, and use the emulator instead.
The docs, issues, and examples I've been able to find on the net are either outdated, or describe how to set up the emulator for testing security rules, or the web frontend.
Attempts using firebase.initializeAdminApp({ projectId: "my-test-project" }); did not do the trick.
I also tried setting FIRESTORE_EMULATOR_HOST=[::1]:8080,127.0.0.1:8080
So the question is: How can I initialise the firebaseApp in my tests, so that my functions are wired up to the firestore emulator?
I had another crack at it today, more than a year later, so some things have changed, which I can't all list out. Here is what worked for me:
1. Install and run the most recent version of firebase-tools and emulators:
$ npm i -g firebase-tools // v9.2.0 as of now
$ firebase init emulators
# You will be asked which emulators you want to install.
# For my purposes, I found the firestore and auth emulators to be sufficient
$ firebase -P <project-id> emulators:start --only firestore,auth
Take note of the ports at which your emulators are available:
2. Testsetup
The purpose of this file is to serve as a setup for tests which rely on emulators. This is where we let our app know where to find the emulators.
// setupFunctions.ts
import * as admin from 'firebase-admin';
// firebase automatically picks up on these environment variables:
process.env.FIRESTORE_EMULATOR_HOST = 'localhost:8080';
process.env.FIREBASE_AUTH_EMULATOR_HOST = 'localhost:9099';
admin.initializeApp({
projectId: 'project-id',
credential: admin.credential.applicationDefault()
});
export const testEnv = require('firebase-functions-test')();
3. Testing a simple function
For this, we setup a simple script which writes a document to firestore. In the test, we assert that the document exists within the emulator, only after we have run the function.
// myFunction.ts
import * as functions from 'firebase-functions';
import {firestore} from 'firebase-admin';
export const myFunction = functions
.region('europe-west1')
.runWith({timeoutSeconds: 540, memory: '2GB'})
.https.onCall(async () => {
await firestore()
.collection('myCollection')
.doc('someDoc')
.set({hello: 'world'});
return {result: 'success'};
});
// myTest.ts
// import testEnv first, to ensure that emulators are wired up
import {testEnv} from './setupFunctions';
import {myFunction} from './myFunction';
import * as admin from 'firebase-admin';
// wrap the function
const testee = testEnv.wrap(myFunction);
describe('myFunction', () => {
it('should add hello world doc', async () => {
// ensure doc does not exist before test
await admin
.firestore()
.doc('myCollection/someDoc')
.delete()
// run the function under test
const result = await testee();
// assertions
expect(result).toEqual({result: 'success'});
const doc = await admin
.firestore()
.doc('myCollection/someDoc')
.get();
expect(doc.data()).toEqual({hello: 'world'});
});
});
And sure enough, after running the tests, I can observe that the data is present in the firestore emulator. Visit http://localhost:4000/firestore while the emulator is running to get this view.