How to setup a firebase firestore and cloud function test suit with firebase Emulator for JS development

To setup a test environment for cloud functions that allows you to simulate read/write and setup test data you have to do the following. Keep in mind, this really simulated/triggers cloud functions. So after you write into firestore, you need to wait a bit until the cloud function is done writing/processing, before you can read the assert the data.

An example repo with the code below can be found here: https://github.com/BrandiATMuhkuh/jaipuna-42-firebase-emulator .

Preconditions

I assume at this point you have a firebase project set up, with a functions folder and index.js in it. The tests will later be inside the functions/test folder. If you don't have project setup use firebase init to setup a project.

Install Dependencies

First add/install the following dependencies: mocha, @firebase/testing, firebase-functions-test, firebase-functions, firebase-admin, firebase-tools into the functions/package.json NOT the root folder.

Replace all jaipuna-42-firebase-emulator names

It's very important that you use your own project-id. It must be the project-id of your own project and must exists. Fake ids won't work. So search for all jaipuna-42-firebase-emulator in the code below and replace it with your project-id.

index.js for an example cloud function

// functions/index.js

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

// init the database
admin.initializeApp(functions.config().firebase);
let fsDB = admin.firestore();

const heartOfGoldRef = admin
    .firestore()
    .collection("spaceShip")
    .doc("Heart-of-Gold");

exports.addCrewMemeber = functions.firestore.document("characters/{characterId}").onCreate(async (snap, context) => {
    console.log("characters", snap.id);

    // before doing anything we need to make sure no other cloud function worked on the assignment already
    // don't forget, cloud functions promise an "at least once" approache. So it could be multiple
    // cloud functions work on it. (FYI: this is called "idempotent")

    return fsDB.runTransaction(async t => {
        // Let's load the current character and the ship
        const [characterSnap, shipSnap] = await t.getAll(snap.ref, heartOfGoldRef);

        // Let's get the data
        const character = characterSnap.data();
        const ship = shipSnap.data();

        // set the crew members and count
        ship.crew = [...ship.crew, context.params.characterId];
        ship.crewCount = ship.crewCount + 1;

        // update character space status
        character.inSpace = true;

        // let's save to the DB
        await Promise.all([t.set(snap.ref, character), t.set(heartOfGoldRef, ship)]);
    });
});


mocha test file index.test.js

// functions/test/index.test.js


// START with: yarn firebase emulators:exec "yarn test --exit"
// important, project ID must be the same as we currently test

// At the top of test/index.test.js
require("firebase-functions-test")();

const assert = require("assert");
const firebase = require("@firebase/testing");

// must be the same as the project ID of the current firebase project.
// I belive this is mostly because the AUTH system still has to connect to firebase (googles servers)
const projectId = "jaipuna-42-firebase-emulator";
const admin = firebase.initializeAdminApp({ projectId });

beforeEach(async function() {
    this.timeout(0);
    await firebase.clearFirestoreData({ projectId });
});

async function snooz(time = 3000) {
    return new Promise(resolve => {
        setTimeout(e => {
            resolve();
        }, time);
    });
}

it("Add Crew Members", async function() {
    this.timeout(0);

    const heartOfGold = admin
        .firestore()
        .collection("spaceShip")
        .doc("Heart-of-Gold");

    const trillianRef = admin
        .firestore()
        .collection("characters")
        .doc("Trillian");

    // init crew members of the Heart of Gold
    await heartOfGold.set({
        crew: [],
        crewCount: 0,
    });

    // save the character Trillian to the DB
    const trillianData = { name: "Trillian", inSpace: false };
    await trillianRef.set(trillianData);

    // wait until the CF is done.
    await snooz();

    // check if the crew size has change
    const heart = await heartOfGold.get();
    const trillian = await trillianRef.get();

    console.log("heart", heart.data());
    console.log("trillian", trillian.data());

    // at this point the Heart of Gold has one crew member and trillian is in space
    assert.deepStrictEqual(heart.data().crewCount, 1, "Crew Members");
    assert.deepStrictEqual(trillian.data().inSpace, true, "In Space");
});


run the test

To run the tests and emulator in one go we navigate into the functions folder and write yarn firebase emulators:exec "yarn test --exit". This command can also be used in your CI pipeline

If it all worked you should see the following output

  √ Add Crew Members (5413ms)

  1 passing (8S)

For anyone struggling with testing firestore triggers, I've made an example repository that will hopefully help other people.

https://github.com/benwinding/example-jest-firestore-triggers

It uses jest and the local firebase emulator.