在 Jest 中模拟 Firebase 管理员时出错:“TypeError:admin.firestore 不是函数”

Ed *_*nds 8 javascript firebase typescript jestjs ts-jest

我有一个函数可以通过 Admin SDK 处理与 Cloud Firestore 的连接。我知道该功能工作正常,因为应用程序连接并允许写入数据库。

现在我正在尝试用 Jest 测试这个功能。为了避免在此函数范围之外进行测试,我模拟了 firebase-admin Node 模块。但是,我的测试失败并出现错误“TypeError:admin.firestore不是函数”。

我的函数和测试都是用 TypeScript 编写的,通过 ts-jest 运行,但我不认为这是 TypeScript 错误,因为 VS Code 没有任何抱怨。我相信这是 Jest 自动模拟的问题。

admin.firebase()是一个有效的调用。TypeScript 定义文件将其定义为function firestore(app?: admin.app.App): admin.firestore.Firestore;

我已经阅读了 Jest 文档,但我不明白如何解决这个问题。

这是我的功能:

// /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
}
Run Code Online (Sandbox Code Playgroud)

这是我的测试代码:

// /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)
  })
})
Run Code Online (Sandbox Code Playgroud)

这是我的相关(或可能相关)package.json(与 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"
  }
}
Run Code Online (Sandbox Code Playgroud)

还有我的笑话配置:

// /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
}
Run Code Online (Sandbox Code Playgroud)

mga*_*cia 12

你的代码看起来不错。jest.mock模拟库的所有方法,默认情况下,所有方法undefined在调用时都会返回。

解释

您看到的问题与firebase-admin模块方法的定义方式有关。

在包的源代码中firebase-admin,该initializeApp方法被定义为以下中的方法FirebaseNamespace.prototype

FirebaseNamespace.prototype.initializeApp = function (options, appName) {
    return this.INTERNAL.initializeApp(options, appName);
};
Run Code Online (Sandbox Code Playgroud)

但是,该firestore方法被定义为属性:

Object.defineProperty(FirebaseNamespace.prototype, "firestore", {
    get: function () {
        [...]
        return fn;
    },
    enumerable: true,
    configurable: true
});
Run Code Online (Sandbox Code Playgroud)

似乎jest.mock能够模拟直接在 中声明的方法(这就是您的调用不会引发错误的prototype原因),但不能模拟定义为属性的方法。admin.initializeApp

解决方案

为了解决这个问题,您可以firestore在运行测试之前添加该属性的模拟:

// /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)
  })
})
Run Code Online (Sandbox Code Playgroud)

替代解决方案

由于之前的解决方案不适合您,因此我会建议一个替代解决方案。firestore您可以定义属性,以便它返回模拟函数,而不是分配方法的值。

为了简化模拟,我将mockFirestoreProperty在您的测试文件中创建一个小助手:

// /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)
  })
})
Run Code Online (Sandbox Code Playgroud)

  • 我尝试了这个解决方案,但 TypeScript 抱怨 ```admin.firestore = jest.fn()```` “无法分配给 'firestore',因为它是只读属性。” 然后,ESLint 的 Jest 插件会自动将其修复为 ```jest.spyOn(admin, "firestore").mockImplementation()```,但随后我的测试失败,并显示“错误:无法监视 firestore 属性,因为它不是一个函数;改为未定义”。尝试监视“firestore()”会出现 TS 错误“没有与此调用匹配的重载。” (2认同)