Firebase functions Unit Testing

Let's walk through a basic unit tests of a function trigger

Fri, 19 Jan 2024

It took me a bit to wrap my head around unit testing with firebase. These are offline tests that being said, Firebase docs do recommend online testing as a preferred method.

This sample will be a bit lengthy, I just wanted to give you guys a real life scenario. This function triggers whenever I want to send a push notifications. The way I do this is by creating a new document in the /notifications firestore location. The notification will have a list of users docIds that will be the receipient of this notification. I grab the users doc, get all of their deviceIds and then use the messaging API to create a new push notification

const admin = require('firebase-admin');
const functions = require('firebase-functions');

// for local dev.  You can use another if/else to depict local mode
if (process.env.USERDOMAIN === "YOURCOMPUTERNAME") {
    admin.initializeApp({ apiKey: "test"});
} else {
    admin.initializeApp();
}

// Subscribe to the notifications/* firestore path for create events
exports.handlePushNotification = functions.firestore.document("notifications/{docId}").onCreate(async (change: any, context: any) => {
    // initializing firestore and creating a batch
    const db = admin.firestore();
    const data = change.data();
    const notificationId = context.params.docId;
    const batch = db.batch();

    try {
        if (!data) {
            console.log("Could not find valid firestore doc for notification");
            return;
        }
        // making sure the notification doc has a valid recipient list
        if (data.recipientIds === undefined || data.recipientIds.length === 0) {
            console.log("Recipient Id list is either undefined or empty");
            return;
        }

        // fetching all the users who are on the recipient list
        const users: any[] = [];
        await Promise.all([...data.recipientIds.map(async(userId: string) => {
            const doc = await db.collection("users").doc(userId).get();
            const user = await doc.data()
            if (user) {
                users.push({...user, id: userId});
            }
        })]);

        // Grabbing all the users device tokens
        const tokens: string[] = [];
        users.forEach(user => {
            if (user.deviceIds && user.deviceIds.length > 0) {
                tokens.push(...user.deviceIds);
            }
        })

        // If we found no tokens, just exit since we can't send a notification without a token
        if (tokens.length <= 0) {
            console.log("No tokens to send notifications to")
            return;
        }

        // Sending the notification.  For failed tokens, we remove the token from the users doc
        const message: any = {
            data: data.data || {},
            tokens,
            notification: data.notification
        };
        await admin.messaging().sendEachForMulticast(message)
            .then(async (response: any) => {
                if (response.failureCount > 0) {
                    for (const resp of response.responses) {
                        const idx = response.responses.indexOf(resp);
                        if (!resp.success) {
                            const user = users.find(el => el.deviceIds && el.deviceIds.includes(tokens[idx]));
                            if (user) {
                                batch.update(db.collection("users").doc(user.id), {
                                    deviceIds: user.deviceIds.filter((el: string) => el !== tokens[idx])
                                });
                            }
                        }
                    }
                }
            });
    } catch(e) {
        console.log("Failed with error: ", e);
    } finally {
        // Committing any updates that we made in our try block along with deleting the doc once we are done
        batch.delete(db.collection('notifications').doc(notificationId));
        await batch.commit();
    }
});

Here is the test. Even though this is an “offline” test, it will not function if you don’t provide a valid config. A lot of this is fairly straightforward, the piece you want to pay the most attention to is the wrapped function. We can provide an array of objects of sample docs that we want to have ran against our test function

const test = require('firebase-functions-test')({
    authDomain: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: ""
}, process.env.GOOGLE_APPLICATION_CREDENTIALS);

const assert = require("assert");

describe('Triggers', function () {
    let functionsToTest;

    // adds the functions to test
    before(function () {
        functionsToTest = require('../lib/index');
    });
    // cleans up firebase after each test
    after(function () {
        test.cleanup();
    });
    describe('Should handle a push notification doc', function () {
        it('It should return true', function (done) {
            const wrapped = test.wrap(functionsToTest.handlePushNotification);
            wrapped({
                    data: () => ({
                        recipientIds: ["2"]
                    })
                },
                {
                    params: {
                        docId: "test"
                    }
                }).then(() => {
                    done();
            });
        });
    });
});
Buy Me A CoffeeDigitalOcean Referral Badge
Loading...
Edward Beazer

Edward Beazer - I just like to build shit. Sometimes I get stuck for hours, even days while trying to figure out how to solve an issue or implement a new feature. Hope my tips and tutorials can save you some time.

DigitalOcean Referral Badge