Error Mocking Firebase Admin in Jest: "TypeError: admin.firestore is not a function" - javascript

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.
Explanation
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.
Solution
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)
})
})
Alternative solution
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)
})
})

It works properly to combine your own mocks with its auto-mocks when you initially mock it, i.e.:
jest.mock('firebase-admin', () => ({
...jest.mock('firebase-admin'),
credential: {
cert: jest.fn(),
},
initializeApp: jest.fn(),
firestore: jest.fn(),
}));

Related

Trying to mock a function return in node.js/express

I'm trying to test an application I'm writing in node.js/Express.
I want to mock the return of a mock-repository I've set up for development purposes.
Mock repo:
let dogs = [];
export const getDogs = () => {
return dogs;
};
export const setDogs = (updatedDogs) => {
dogs = updatedDogs;
};
I'm importing the function to mock a return value from in my test file;
import * as sut from "../dogs.js";
import * as dogsRepo from "../../repositories/dogs.js";
import { mockRequest, mockResponse } from "mock-req-res";
import { jest } from "#jest/globals";
describe("dogs controller", () => {
describe("getDogs", () => {
test("should return dogs", () => {
// Arrange
const req = mockRequest();
const res = mockResponse();
// dogsRepo.getDogs = jest.fn().mockReturnValue([]);
// jest.spyOn(dogsRepo, "getDogs").mockReturnValue([]);
jest.spyOn(dogsRepo, "getDogs").mockImplementation(() => []);
//Act
sut.getDogs(req, res);
});
});
});
I simply want my test to mock a return value of [] from the repository. I have done this many times in react projects but this is my first time doing so in node.js.
I have tried to mock the return of the function in three different ways and each one gives me the error;
"TypeError: Cannot assign to read only property 'getDogs' of object '[object Module]'"
I import in the same way in the controller (sut) and it works as expected so I can feel reasonably confident that the style of import/export I'm employing is working.
import * as dogsRepo from "../repositories/dogs.js";
export const getDogs = (req, res) => {
const dogs = dogsRepo.getDogs();
res.send(dogs);
};
What am I doing wrong?
Could it have something to do with this configuration in my package.json?
"scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"start": "nodemon index.js"
},
As far as I can see you are not mocking the module. Try something like the following:
jest.mock('../../repositories/dogs');
And your test should work. for more info, see the official jest documentation.

How to call a firebase functions from a unit test using Firebase Emulators?

I have the firebase emulators running.
My application can call a firebase cloud function that can create a custom user claim if the user is allowed to (depending on a value in the database)
I'm trying to invoke that function:
import * as firebase from '#firebase/testing';
import * as fs from 'fs';
const app = firebase.initializeTestApp({
databaseName: 'mydb',
auth: {
uid: 'useruid'
},
});
describe('Firebase Functions', () => {
beforeEach(async () => {
await firebase.loadDatabaseRules({
databaseName: 'mydb',
rules: fs.readFileSync('database.rules.json', 'utf8'),
});
});
afterAll(async () => {
await Promise.all(firebase.apps().map((app) => app.delete()));
});
const cloudfunction = app.functions().httpsCallable('cloudfunction')
it('throws error if user is not authenticated', async () => {
await firebase.assertFail(cloudfunction())
})
});
This is throwing an error:
Error: Error: Response for preflight has invalid HTTP status code 404
In my opinion, you don't need to call firebase from your unit test, because it is not your business is firebase working correctly or not, you need to test your business logic so that means that you will mock the method which calls to the firebase and test expectable returning values of this method, for example, you can write 2 cases when notification will be sent without error and second if firebase service will return failure, for those 2 cases your code should have appropriate behavior.
Testing firebase service will be done by the team working on the firebase
There exists firebase-functions-test for this. This allows you to wrap your cloud functions and call them directly, as well as create stubs for document updates. When running in the context of the emulator, the firebase-admin SDK still interacts with the emulator. A basic setup might look like the following (assuming typescript):
// functions/src/test/functions.test.ts
import {expect} from "chai";
import "mocha";
import firebaseFunctionTest from "firebase-functions-test";
import * as cf from "../index";
const projectId = "demo-project";
const test = firebaseFunctionTest({
projectId,
});
after(() => test.cleanup());
describe("cloud functions", () => {
afterEach(async () => {
await test.firestore.clearFirestoreData(projectId);
});
describe("myFunction", () => {
const myFunction = test.wrap(cf.myFunction);
// Create mock data with the admin SDK and test function.
}
}
In your package.json you can the put a script like.
"scripts": {
"test:unit": "mocha -r ts-node/register --timeout 5000 --exit src/**/*.test.ts",
"test": "npm run build && npx firebase -P demo-project emulators:exec 'npm run test:unit'",
...
}
You might also want to have a look at https://github.com/firebase/quickstart-testing/tree/master/unit-test-cloud-functions
and https://firebase.google.com/docs/functions/unit-testing.

ES6 imports and 'is not a constructor' in Jest.mock

Similar to Jest TypeError: is not a constructor in Jest.mock, except I am using ES6 imports - and the answer given to that question is not working on my situation.
Following the Jest .mock() documentation I am attempting to mock the constructor Client from the pg module.
I have a constructor, Client, imported from an ES6 module called pg. Instances of Client should have a query method.
import { Client } from "pg";
new Client({ connectionString: 'postgresql://postgres:postgres#localhost:5432/database' });
export async function doThing(client): Promise<string[]> {
var first = await client.query('wooo')
var second = await client.query('wooo')
return [first, second]
}
Here's my __tests__/test.ts
const log = console.log.bind(console)
jest.mock("pg", () => {
return {
query: jest
.fn()
.mockReturnValueOnce('one')
.mockReturnValueOnce('two'),
};
});
import { Client } from "pg";
import { doThing } from "../index";
it("works", async () => {
let client = new Client({});
var result = await doThing(client);
expect(result).toBe(['one', 'two'])
});
This is similar to the answer given in Jest TypeError: is not a constructor in Jest.mock, but it's failing here.
The code, just:
const mockDbClient = new Client({ connectionString: env.DATABASE_URL });
fails with:
TypeError: pg_1.Client is not a constructor
I note the docs mention __esModule: true is required when using default exports, but Client is not a default export from pg (I've checked).
How can I make the constructor work properly?
Some additional notes after getting an answer
Here's a slightly longer-form version of the answer, with comments about what's happening on each line - I hope people reading this find it useful!
jest.mock("pg", () => {
// Return the fake constructor function we are importing
return {
Client: jest.fn().mockImplementation(() => {
// The consturctor function returns various fake methods
return {
query: jest.fn()
.mockReturnValueOnce(firstResponse)
.mockReturnValueOnce(secondResponse),
connect: jest.fn()
}
})
}
})
When you mock the module, it needs to have the same shape as the actual module. Change:
jest.mock("pg", () => {
return {
query: jest
.fn()
.mockReturnValueOnce('one')
.mockReturnValueOnce('two'),
};
});
...to:
jest.mock("pg", () => ({
Client: jest.fn().mockImplementation(() => ({
query: jest.fn()
.mockReturnValueOnce('one')
.mockReturnValueOnce('two')
}))
}));

Stubbing express middleware functions with sinon is

Description
I try to set stub fakes for a express middleware function and it's not replacing over.
What I'm trying (how to reproduce)
I'm trying to use sinon stubbing via callsFake function, just as it's advised from their most updated docs.
Even though I'm requiring the module and replacing the function from the property at the export. I keep seeing the original function behavior acting.
I know that I should try to get the function stubbed before the middleware functions get setup, and that's when express app is first imported.
This is the function I'm trying to stub, defined as a function and exported as a object too. It's defined in a script file with a path like api/middlewares/stripe/signature.
const stripeHelper = require('../../../lib/stripe')
const logger = require('../../../lib/logger')
const verifySignature = (req, res, next) => {
var event
let eventName = req.url.replace('/', '')
try {
// Try adding the Event as `request.event`
event = stripeHelper.signatureCheck(
eventName,
req.body,
req.headers['stripe-signature']
)
} catch (e) {
// If `constructEvent` throws an error, respond with the message and return.
logger.error('Error while verifying webhook request signature', e.message, e)
return res.status(400).send('Webhook Error:' + e.message)
}
req.event = event
next()
}
module.exports.verifySignature = verifySignature
What I tried already
Use decache to make sure the express app instance is pristine and it's not being initialized with previous original middleware
Set multiple beforEach hooks in order to organize my stubs and preconditions or test
What keeps happening
The original middleware function gets executed
I don't see any logs of the stub functions (as second proof that sinon stub is not working
This is my stubs and test hooks setup:
const chai = require('chai')
const chaiHttp = require('chai-http')
const dirtyChai = require('dirty-chai')
const sinon = require('sinon')
const decache = require('decache')
const signatureMiddleware = require('../../../api/middlewares/stripe/signature')
const bp = require('body-parser')
let verifySignatureStub, rawStub
chai.should()
chai.use(dirtyChai)
chai.use(chaiHttp)
const API_BASE = '/api/subscriptions'
const planId = 'NYA-RUST-MONTHLY'
const utils = require('../../utils')
const {
hooks: {createSubscription, emitPaymentSucceeded},
stripe: {generateEventFromMock}
} = utils
let testUser, testToken, testSubscription, server
describe.only('Subscriptions renewal (invoice.payment_succeeded)', function () {
this.timeout(30000)
beforeEach(function (done) {
createSubscription(server, {planId}, function (err, resp) {
if (err) return done(err)
const {user, jwt, subscription} = resp
console.log(user, jwt)
testUser = user
testToken = jwt
testSubscription = subscription
done()
})
})
beforeEach(function (done) {
verifySignatureStub = sinon.stub(signatureMiddleware, 'verifySignature')
rawStub = sinon.stub(bp, 'raw')
rawStub.callsFake(function (req, res, next) {
console.log('bp raw')
return next()
})
verifySignatureStub.callsFake(function (req, res, next) {
const {customerId} = testUser.stripe
const subscriptionId = testSubscription.id
console.log('fake verify')
req.event = generateEventFromMock('invoice.payment_failed', {subscriptionId, customerId, planId})
return next()
})
done()
})
beforeEach(function (done) {
decache('../../../index')
server = require('../../../index')
const {customerId} = testUser.stripe
const {id: subscriptionId} = testSubscription
console.log(`emitting payment succeeded with ${customerId}, ${subscriptionId} ${planId}`)
emitPaymentSucceeded(server, testToken, function (err, response) {
if (err) return done(err)
done()
})
})
afterEach(function (done) {
verifySignatureStub.restore()
done()
})
it('Date subscription will renew gets set to a valid number roughly one month', function () {
// Not even getting here becasue calling the original function contains verifyMiddleware which should be replaced
})
it('Current period end is modified')
it('An invoice for the new starting period is generated')
it('Subscription status keeps active')
})
Context (please complete the following information):
All runs over Node 8 and I'm running tests with mocha and did a set up with dirty chai.
These are my dev dependencies:
"devDependencies": {
"base64url": "^2.0.0",
"cross-env": "^5.0.5",
"decache": "^4.4.0",
"dirty-chai": "^2.0.1",
"faker": "^4.1.0",
"google-auth-library": "^0.12.0",
"googleapis": "^23.0.0",
"minimist": "^1.2.0",
"mocha": "^5.2.0",
"nodemon": "^1.12.0",
"nyc": "^11.2.1",
"sinon": "^6.1.5",
"standard": "^10.0.3",
"stripe-local": "^0.1.1"
}
Open issue
https://github.com/sinonjs/sinon/issues/1889
As a rule of thumb, stubs should be set up per test, i.e. in beforeEach or it, not in before. Here they don't seem to contain per-test logic but they can, and in this case they won't work as expected with before. mocha-sinon should preferably be used to integrate Mocha with Sinon sandbox, so no afterEach is needed to restore stubs, this is done automatically.
Since verifySignature is export property and not the export itself, signatureMiddleware module may be left as is, but modules that use it should be de-cached and re-imported in tests where they are expected to use verifySignature. If the behaviour should be same for entire test suite, this should be performed in beforeEach as well. E.g. if these middlewares are used in app module directly, it's:
const decache = require('decache');
...
describe(() => {
let app;
beforeEach(() => {
verifySignatureStub = sinon.stub(signatureMiddleware, 'verifySignature');
...
});
beforeEach(() => {
decache('./app');
app = require('./app');
});

Test module.hot with Jest

I'm trying to get coverage to 100% for a module with hot module reloading setup.
In my module I have this:
// app.js
if (module && module.hot) module.hot.accept();
In the test file I am trying to do this
// app.test.js
it('should only call module.hot.accept() if hot is defined', () => {
const accept = jest.fn();
global.module = { hot: { accept } };
jest.resetModules();
require('./app');
expect(accept).toHaveBeenCalled();
}
);
But when I log out module in app.js it shows the require stuff but doesn't contain the hot method set by test.
If you have a variable referencing the module object, then you can inject a mock module object into that variable for the test. For instance, you could do the following:
// app.js
// ...
moduleHotAccept(module);
// ...
export function moduleHotAccept(mod) {
if (mod && mod.hot) {
mod.hot.accept();
}
}
Which can be tested like so:
// app.test.js
import { moduleHotAccept } from './app'
it('should only call hot.accept() if hot is defined', () => {
const accept = jest.fn();
const mockModule = { hot: { accept } };
moduleHotAccept(mockModule);
expect(accept).toHaveBeenCalled();
}
);
it('should not throw if module is undefined', () => {
expect(moduleHotAccept).not.toThrow();
}
);
it('should not throw if module.hot is undefined', () => {
expect(
() => moduleHotAccept({notHot: -273})
).not.toThrow();
}
);
I've needed it as well without the ability to pass it from outside.
My solution was to use a jest "transform" that allows me to modify a bit the code of the file that is using module.hot.
So in order to setup it you need to add:
// package.json
"transform": {
"file-to-transform.js": "<rootDir>/preprocessor.js"
//-------^ can be .* to catch all
//------------------------------------^ this is a path to the transformer
},
Inside preprocessor.js,
// preprocessor.js
module.exports = {
process(src, path) {
if( path.includes(... the path of the file that uses module.hot)) {
return src.replace('module.hot', 'global.module.hot');
}
return src;
},
};
That transformer will replace module.hot to global.module.hot, that means that you can control it value in the tests like so:
// some-test.spec.js
global.module = {
hot: {
accept: jest.fn,
},
};
Hope that it helps.

Categories

Resources