I have a function to handle connecting to Cloud Firestore through the Admin SDK. I know the function works fine, as the app connects and allows writing to the database.
Now I am trying to test this function with Jest. To avoid testing outside the scope of this function, I am mocking the firebase-admin Node module. However, my test is failing with the error "TypeError: admin.firestore is not a function".
My function and tests are both written in TypeScript, run via ts-jest, but I don't think this is a TypeScript error, as VS Code has no complaints. I believe this is an issue with Jest's automatic mocking.
admin.firebase()
is a valid call. The TypeScript definition file defines it as function firestore(app?: admin.app.App): admin.firestore.Firestore;
I've read over the Jest docs, but I'm not understanding how to fix this.
This is my function:
// /src/lib/database.ts
import * as admin from "firebase-admin"
/**
* Connect to the database
* @param key - a base64 encoded JSON string of serviceAccountKey.json
* @returns - a Cloud Firestore database connection
*/
export function connectToDatabase(key: string): FirebaseFirestore.Firestore {
// irrelevant code to convert the key
try {
admin.initializeApp({
credential: admin.credential.cert(key),
})
} catch (error) {
throw new Error(`Firebase initialization failed. ${error.message}`)
}
return admin.firestore() // this is where it throws the error
}
Here is my test code:
// /tests/lib/database.spec.ts
jest.mock("firebase-admin")
import * as admin from "firebase-admin"
import { connectToDatabase } from "@/lib/database"
describe("database connector", () => {
it("should connect to Firebase when given valid credentials", () => {
const key = "ewogICJkdW1teSI6ICJUaGlzIGlzIGp1c3QgYSBkdW1teSBKU09OIG9iamVjdCIKfQo=" // dummy key
connectToDatabase(key) // test fails here
expect(admin.initializeApp).toHaveBeenCalledTimes(1)
expect(admin.credential.cert).toHaveBeenCalledTimes(1)
expect(admin.firestore()).toHaveBeenCalledTimes(1)
})
})
Here are my relevant (or possibly relevant) package.json (installed with Yarn v1):
{
"dependencies": {
"@firebase/app-types": "^0.6.0",
"@types/node": "^13.13.5",
"firebase-admin": "^8.12.0",
"typescript": "^3.8.3"
},
"devDependencies": {
"@types/jest": "^25.2.1",
"expect-more-jest": "^4.0.2",
"jest": "^25.5.4",
"jest-chain": "^1.1.5",
"jest-extended": "^0.11.5",
"jest-junit": "^10.0.0",
"ts-jest": "^25.5.0"
}
}
And my jest config:
// /jest.config.js
module.exports = {
setupFilesAfterEnv: ["jest-extended", "expect-more-jest", "jest-chain"],
preset: "ts-jest",
errorOnDeprecated: true,
testEnvironment: "node",
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
},
moduleFileExtensions: ["ts", "js", "json"],
testMatch: ["<rootDir>/tests/**/*.(test|spec).(ts|js)"],
clearMocks: true,
}
Your code looks good. jest.mock
mocks all the methods of the library and, by default, all of them will return undefined
when called.
The problem you are seeing is related to how the firebase-admin
module methods are being defined.
In the source code for firebase-admin
package, the initializeApp
method is being defined as a method in the FirebaseNamespace.prototype
:
FirebaseNamespace.prototype.initializeApp = function (options, appName) {
return this.INTERNAL.initializeApp(options, appName);
};
However, the firestore
method is being defined as a property:
Object.defineProperty(FirebaseNamespace.prototype, "firestore", {
get: function () {
[...]
return fn;
},
enumerable: true,
configurable: true
});
It seems that jest.mock
is able to mock the methods declared directly in the prototype
(that's the reason why your call to admin.initializeApp
does not throw an error) but not the ones defined as properties.
To overcome this problem you can add a mock for the firestore
property before running your test:
// /tests/lib/database.spec.ts
import * as admin from "firebase-admin"
import { connectToDatabase } from "@/lib/database"
jest.mock("firebase-admin")
describe("database connector", () => {
beforeEach(() => {
// Complete firebase-admin mocks
admin.firestore = jest.fn()
})
it("should connect to Firebase when given valid credentials", () => {
const key = "ewogICJkdW1teSI6ICJUaGlzIGlzIGp1c3QgYSBkdW1teSBKU09OIG9iamVjdCIKfQo=" // dummy key
connectToDatabase(key) // test fails here
expect(admin.initializeApp).toHaveBeenCalledTimes(1)
expect(admin.credential.cert).toHaveBeenCalledTimes(1)
expect(admin.firestore).toHaveBeenCalledTimes(1)
})
})
Since the previous solution did not work for you, I'll suggest an alternative solution. Instead of assigning the value of the firestore
method you can define the property so that it returns a mocked function.
To simplify the mock, I would create a little helper mockFirestoreProperty
in your test file:
// /tests/lib/database.spec.ts
import * as admin from "firebase-admin"
import { connectToDatabase } from "@/lib/database"
jest.mock("firebase-admin")
describe("database connector", () => {
// This is the helper. It creates a mock function and returns it
// when the firestore property is accessed.
const mockFirestoreProperty = admin => {
const firestore = jest.fn();
Object.defineProperty(admin, 'firestore', {
get: jest.fn(() => firestore),
configurable: true
});
};
beforeEach(() => {
// Complete firebase-admin mocks
mockFirestoreProperty(admin);
})
it("should connect to Firebase when given valid credentials", () => {
const key = "ewogICJkdW1teSI6ICJUaGlzIGlzIGp1c3QgYSBkdW1teSBKU09OIG9iamVjdCIKfQo=" // dummy key
connectToDatabase(key) // test fails here
expect(admin.initializeApp).toHaveBeenCalledTimes(1)
expect(admin.credential.cert).toHaveBeenCalledTimes(1)
expect(admin.firestore).toHaveBeenCalledTimes(1)
})
})