Búsqueda de sitios web

javascript - Error al simular Firebase Admin en Jest: "TypeError: admin.firestore no es una función"


Tengo una función para manejar la conexión a Cloud Firestore a través del Admin SDK. Sé que la función funciona bien, ya que la aplicación se conecta y permite escribir en la base de datos.

Ahora estoy intentando probar esta función con Jest. Para evitar probar fuera del alcance de esta función, estoy simulando el módulo Node firebase-admin. Sin embargo, mi prueba falla con el error "TypeError: admin.firestore no es una función".

Mi función y las pruebas están escritas en TypeScript, ejecutadas mediante ts-jest, pero no creo que esto sea un error de TypeScript, ya que VS Code no muestra quejas. Creo que esto es un problema con el mock automático de Jest.

admin.firebase() es una llamada válida. El archivo de definición de TypeScript la define como function firestore(app?: admin.app.App): admin.firestore.Firestore;

He leído la documentación de Jest, pero no entiendo cómo solucionar esto.

Esta es mi función:

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

Aquí está mi código de prueba:

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

Aquí están mis package.json relevantes (o posiblemente relevantes) (instalados con 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"
  }
}

Y mi configuración de jest:

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

Respuesta #1

Tu código se ve bien. jest.mock simula todos los métodos de la biblioteca y, por defecto, todos ellos devolverán undefined cuando se llamen.

Explicación

El problema que está viendo está relacionado con la forma en que se están definiendo los métodos del módulo firebase-admin.

En el código fuente del paquete firebase-admin, el método initializeApp se está definiendo como un método en el FirebaseNamespace.prototype:

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

Sin embargo, el método firestore se está definiendo como una propiedad:

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

Parece que jest.mock es capaz de simular los métodos declarados directamente en el prototype (esa es la razón por la que tu llamada a admin.initializeApp no lanza un error), pero no los que están definidos como propiedades.

Solución

Para superar este problema, puedes agregar un mock para la propiedad firestore antes de ejecutar tu prueba:

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

Solución alternativa

Dado que la solución anterior no funcionó para ti, sugeriré una solución alternativa. En lugar de asignar el valor del método firestore, puedes definir la propiedad para que devuelva una función simulada.

Para simplificar el mock, crearía un pequeño ayudante mockFirestoreProperty en tu archivo de prueba:

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

Respuesta #2

Funciona correctamente combinar tus propios mocks con sus auto-mocks cuando lo mockeas inicialmente, es decir:

jest.mock('firebase-admin', () => ({
  ...jest.mock('firebase-admin'),
  credential: {
    cert: jest.fn(),
  },
  initializeApp: jest.fn(),
  firestore: jest.fn(),
}));

Artículos relacionados