2

I'm writing jest unit tests for my slack bot event handler functions. I'm not testing interaction with the database; I've mocked the function which calls the database, so no actual database calls happen during the tests.

However, because firebase initialised in firebase.ts which is imported into index.ts, the test suite fails to run because jest doesn't have the correct environment variables.

What I want to achieve is a way of not instantiating the database at all, when I'm running tests. I've looked at some options for mocking firebase (eg npm packages such ts-mock-firebase, firebase-mock, and more manual approach using some other SO answers), but I haven't had any success with them.

So, is there a safe, reasonable way I can stop the database from instantiating during tests? It feels like this should be easy - maybe I'm missing something obvious!

I'm really surprised there doesn't seem to be a kind of community accepted solution to this problem as far as I can tell. Maybe I'm wrong and I've been looking in the wrong places?

Thanks for any wisdom you can offer!

folder structure

  - index.ts
  database
    - firebase.ts
  services
    - events.ts
    - events.test.ts

index.ts


import { loadEnv } from "./config/dotenv";
loadEnv();

import "./database/firebase";

// some app config here...

export app = new App(appOptions); 

firebase.ts


/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-var-requires */
import * as admin from "firebase-admin";
import firebaseServiceKey from "../config/firebaseServiceKey";

admin.initializeApp({
  // current required type appears to be wrong so has to be cast as any
  credential: admin.credential.cert(firebaseServiceKey as any),
  databaseURL:
    process.env.FIREBASE_URL || "https://loopin-c782d.firebaseio.com",
});

export const auth = admin.auth();

export const firestore = admin.firestore();

events.ts

/**
 * A user renames the organization name in slack
 */
export const handleTeamRename = async (
  userId: string,
  token: string
): Promise<void> => {
  try {
    const team = await getSlackTeam(token, userId);
    await updateOrganizationName(team.id, team.name, team.icon.image_230);
  } catch (e) {
    console.error("team_rename event");
    console.error(e);
  }
};

events.test.ts

import * as events from "./Events";
import * as dbOrganizations from "../database/Organizations";
import * as dbSlack from "../database/Slack";
import { SlackGeneratedTeam } from "../models/Slack";


describe("events", () => {
  const fakeBotToken = "xoxb-123abc";
  const fakeOrgId = "fakeOrgId";
  const fakeOrgName = "Mega Corp, inc.";
  const fakeUrl = "www.example.com";
  const fakeSlackTeam = {
    id: fakeOrgId,
    name: fakeOrgName,
    icon: {
      image_230: fakeUrl,
    },
  };

  describe("handleTeamRename", () => {
    let mockGetSlackTeam: jest.SpyInstance<
      Promise<SlackGeneratedTeam>,
      [token: string, id: string]
    >;
    let mockUpdateOrganizationName: jest.SpyInstance<
      Promise<void>,
      [id: string, displayName: string, photoUrl: string]
    >;

    beforeEach(() => {
      mockGetSlackTeam = jest.spyOn(dbSlack, "getSlackTeam").mockImplementation(
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        async (token: string, id: string) => fakeSlackTeam as SlackGeneratedTeam
      );
      mockUpdateOrganizationName = jest
        .spyOn(dbOrganizations, "updateOrganizationName")
        .mockImplementation(
          // eslint-disable-next-line @typescript-eslint/no-unused-vars
          async (id: string, displayName: string, photoUrl: string) => {
            return;
          }
        );
    });

    afterEach(() => {
      mockGetSlackTeam.mockRestore();
      mockUpdateOrganizationName.mockRestore();
    });

    it("should make the right calls", async () => {
      void (await events.handleTeamRename(fakeOrgId, fakeBotToken));
      await expect(mockGetSlackTeam).toHaveBeenCalledTimes(1);
      await expect(mockGetSlackTeam).toHaveBeenCalledWith(
        fakeBotToken,
        fakeOrgId
      );
      await expect(mockUpdateOrganizationName).toHaveBeenCalledTimes(1);
      await expect(mockUpdateOrganizationName).toHaveBeenCalledWith(
        fakeOrgId,
        fakeOrgName,
        fakeUrl
      );
    });
  });
});

error

 npm run test events

> api@1.0.0 test
> jest "events"

 FAIL  src/services/Events.test.ts
  ● Test suite failed to run

    Service account object must contain a string "project_id" property.

       6 | admin.initializeApp({
       7 |   // current required type appears to be wrong so has to be cast as any
    >  8 |   credential: admin.credential.cert(firebaseServiceKey as any),
         |                                ^
       9 |   databaseURL:
      10 |     process.env.FIREBASE_URL || "[my firebase url]",
      11 | });

      at FirebaseAppError.FirebaseError [as constructor] (node_modules/firebase-admin/lib/utils/error.js:43:28)
      at FirebaseAppError.PrefixedFirebaseError [as constructor] (node_modules/firebase-admin/lib/utils/error.js:89:28)
      at new FirebaseAppError (node_modules/firebase-admin/lib/utils/error.js:124:28)
      at new ServiceAccount (node_modules/firebase-admin/lib/credential/credential-internal.js:135:19)
      at new ServiceAccountCredential (node_modules/firebase-admin/lib/credential/credential-internal.js:69:15)
      at Object.cert (node_modules/firebase-admin/lib/credential/credential.js:113:54)
      at Object.<anonymous> (src/database/firebase.ts:8:32)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        3.154 s, estimated 4 s
Ran all test suites matching /events/i.
sauntimo
  • 1,531
  • 1
  • 17
  • 28

1 Answers1

1

Change your firebase.ts like below - notice the if (!admin.apps.length) check before initialising the admin:

import * as admin from "firebase-admin";
import firebaseServiceKey from "../config/firebaseServiceKey";

if (!admin.apps.length) { /** See https://stackoverflow.com/a/57764002/2516673 */
  admin.initializeApp({
    // current required type appears to be wrong so has to be cast as any
    credential: admin.credential.cert(firebaseServiceKey as any),
    databaseURL: process.env.FIREBASE_URL || "https://loopin-c782d.firebaseio.com",
  });
}

export const auth = admin.auth();
export const firestore = admin.firestore();

Then you just need to mock admin.apps at the beginning of your events.test.ts file:

jest.mock("firebase-admin", () => {
  return {
    apps: ["testAppId"], /** this array should not be empty, so firebase-admin won't try to load a certificate when running unit tests */
    auth: jest.fn(),
    firestore: jest.fn(),
  };
});

describe(...);

Now not only you can mock your admin, but you're also possibly avoiding a known issue with multiple initialisation calls.

Fappaz
  • 3,033
  • 3
  • 28
  • 39