Ошибка имитации администратора Firebase в Jest: "TypeError: admin.firestore не является функцией" - PullRequest
1 голос
/ 08 мая 2020

У меня есть функция для обработки подключения к Cloud Firestore через Admin SDK. Я знаю, что функция работает нормально, так как приложение подключается и позволяет записывать в базу данных.

Теперь я пытаюсь протестировать эту функцию с помощью Jest. Чтобы избежать тестирования, выходящего за рамки этой функции, я высмеиваю модуль узла firebase-admin. Однако мой тест не проходит с ошибкой «TypeError: admin.firestore не является функцией».

Моя функция и тесты написаны на TypeScript, запускаются через ts-jest, но я не думаю, что это это ошибка TypeScript, так как VS Code не имеет претензий. Я считаю, что это проблема с автоматическим c насмешкой 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
}

Вот мой тестовый код:

// /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)
  })
})

Вот мой соответствующий (или, возможно, соответствующий) пакет. 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"
  }
}

И мой шуточный конфиг:

// /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,
}

1 Ответ

2 голосов
/ 09 мая 2020

Ваш код выглядит хорошо. jest.mock имитирует все методы библиотеки и по умолчанию все они возвращают undefined при вызове.

Объяснение

Проблема, которую вы видите связано с тем, как определяются методы модуля firebase-admin.

В исходном коде для пакета firebase-admin метод initializeApp определяется как метод в FirebaseNamespace.prototype:

FirebaseNamespace.prototype.initializeApp = function (options, appName) {
    return this.INTERNAL.initializeApp(options, appName);
};

Однако метод firestore определяется как свойство:

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

Кажется, что 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)
  })
})

Альтернативное решение

Поскольку предыдущее решение не помогло вам, я предлагаю альтернативное решение. Вместо присвоения значения методу 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)
  })
})
...