Testing Firebase Functions with Firestore
Figuring out how to set up testing for Firebase functions that use Firestore has been a nightmare. The Firebase docs have been out of date for almost a year. Here's how I got it working. My versions:
"firebase-admin": "^12.1.0",
"firebase-functions": "^6.0.1",
"firebase-functions-test": "^3.3.0",
and firebase
(the cli) version 8.10.0. Hopefully this helps until the next major bump.
I'm trying to test a function
import { FirestoreEvent, onDocumentCreated } from "firebase-functions/v2/firestore";
import * as logger from "firebase-functions/logger";
import {initializeApp} from "firebase-admin/app";
import { getFirestore, QueryDocumentSnapshot } from "firebase-admin/firestore";
export const app = initializeApp({ projectId: 'my-app' });
type Event = FirestoreEvent<QueryDocumentSnapshot | undefined>;
export const myFunction = onDocumentCreated(
"/products/{documentId}",
async (event: Event) => {
if (event.data) {
const statusDoc = getFirestore().doc(`/${event.document}/statuses/current`);
const status = await statusDoc.get();
logger.log(event.data.data());
logger.log(status.data());
}
}
);
Using the npm script
"test": "firebase emulators:exec --only firestore jest"
I run the following test
import * as admin from 'firebase-admin';
import * as logger from "firebase-functions/logger";
import * as firebaseFunctionsTest from 'firebase-functions-test';
const test = firebaseFunctionsTest({ projectId: 'my-app' });
// I read somewhere that it's important to do the require after
// initializing `firebase-functions-test`, so that it will pick
// up some configuration overrides or something. It doesn't seem
// to matter in my setup, but maybe it will for yours.
const { myFunction } = require('./index');
const db = admin.firestore();
describe('myFunction', () => {
let loggerSpy: jest.SpyInstance;
beforeAll(() => {
if (!admin.apps.length) {
admin.initializeApp();
}
});
beforeEach(() => {
loggerSpy = jest.spyOn(logger, 'log');
});
afterAll(async () => {
await Promise.all(admin.apps.map(app => app!.delete()));
await deleteCollection('products');
await deleteCollection('statuses');
});
it('runs my function and gets both event and status data', async () => {
wrapped = test.wrap(myFunction);
const mockProduct: any = {
name: 'Widget 2000'
};
const { productSnapshot } = await createProduct(mockProduct);
// This event just gets passed through to the function
const event = { data: productSnapshot, document: 'products/product1' }
await wrapped(event);
expect(loggerSpy).toHaveBeenCalledWith(expect.any(Object));
});
});
/** This function creates the event and status documents
* in the emulated firestore */
export async function createProduct(product: object) {
const status = { status: "pending" };
const productRef = db.collection('products').doc('product1')
await productRef.set(product);
const statusRef = productRef.collection('statuses').doc('current');
await statusRef.set(status);
const productSnapshot = await productRef.get();
const statusSnapshot = await statusRef.get();
return { productSnapshot, statusSnapshot };
}
/** Clean up! */
export async function deleteCollection(collection: string) {
const snapshot = await db.collection(collection).get()
for (const doc of snapshot.docs) {
await doc.ref.delete();
}
}
Since we are using the firestore emulator, we can be confident that the function behavior will be pretty realistic.