So I'm working on the tests for Google Cloud Functions and I'm having difficulties to stub node_modules libraries with different behaviors.
At the beginning of the test file I have a stub for the firebase-admin
library on the boforeAll
.
Then I have to different tests it should export idinInitRequest
and idin issue
with it's correspondent require of the file being tested, '../src/index'
, which is the cloud function.
First one works as expected. Stubbing with my callsFake
correctly.
The problem comes at the second test. In the second test I'm mocking another library that is being used in the Cloud Function node-idin-beta
. In this test, the firebase-admin
library is not being stubbed. In fact, it's behaving as it was not stubbed at all, using all its default methods. A console.log(admin)
inside the function shows this:
console.log src/app.ts:122
{ firestore: [Function: firestore], auth: [Function: auth] }
console.log src/app.ts:122
FirebaseApp {
firebaseInternals_:
FirebaseNamespaceInternals {
firebase_:
FirebaseNamespace {
__esModule: true,
credential: [Object],
SDK_VERSION: '6.3.0',
Promise: [Function: Promise],
INTERNAL: [Circular],
(...)
We can see from the log that in the first execution it's stubbing right but not in the second.
The reasoning behind requiring the function on each test is because of the different behaviors the libraries might have under the different tests.
I don't know how to stub firebase-admin
once again after the first require.
Here I leave a piece of the code:
import { ContextOptions } from 'firebase-functions-test/lib/main';
import setup from './lib/setup.lib';
const { admin, sinon, assert, testEnv } = setup;
describe('Cloud Functions', (): void => {
let myFunctions;
let adminInitStub;
beforeAll((): void => {
// [START stubAdminInit]
// If index.js calls admin.initializeApp at the top of the file,
// we need to stub it out before requiring index.js. This is because the
// functions will be executed as a part of the require process.
// Here we stub admin.initializeApp to be a dummy function that doesn't do anything.
adminInitStub = sinon.stub(admin, 'initializeApp');
process.env.FUNCTION_NAME = 'idinInitRequest';
// [END stubAdminInit]
});
afterAll((): void => {
// Restore admin.initializeApp() to its original method.
adminInitStub.restore();
// Do other cleanup tasks.
process.env.FUNCTION_NAME = '';
myFunctions = undefined;
testEnv.cleanup();
});
afterEach((): void => {
myFunctions = undefined;
// Restore mocks
jest.resetModules();
});
describe('idinInitRequest', (): void => {
it('it should export idinInitRequest', (): void => {
adminInitStub = adminInitStub.callsFake((): any => ({
firestore: (): any => ({
settings: (): void => {},
}),
auth: (): void => { },
}));
myFunctions = require('../src/index');
const cFunction = require('../src/idinInitRequest');
assert.isObject(myFunctions);
assert.include(myFunctions, { idinInitRequest: cFunction });
});
it('idin issue', async (): Promise<void> => {
jest.mock('node-idin-beta', (): { [key: string]: any } => ({
getTransactionResponse: (): Promise<any> => Promise.reject('shiat'),
}));
adminInitStub = adminInitStub.callsFake((): any => ({
firestore: (): any => ({
settings: (): void => {},
}),
auth: (): void => { },
}));
myFunctions = require('../src/index');
const wrapped = testEnv.wrap(myFunctions.idinInitRequest);
const onCallObjects: [any, ContextOptions] = [
{ issuerId: 'issuer', merchantReturnUrl: 'url' },
{ auth: { uid: '32344' } },
];
await expect(wrapped(...onCallObjects)).rejects.toThrow('There was an error connecting to the idin issuer');
});
});
});
Following the explanation of the difference between stub and mock here, I decided to mock firebase-admin
too because yes, it's a predefined behavior but it will change depending on the test. And it's working. Here it is the code:
describe('Cloud Functions', (): void => {
let myFunctions;
beforeAll((): void => {
process.env.FUNCTION_NAME = 'idinInitRequest';
});
afterAll((): void => {
process.env.FUNCTION_NAME = '';
myFunctions = undefined;
testEnv.cleanup();
});
afterEach((): void => {
myFunctions = undefined;
// Restore mocks
jest.resetModules();
});
describe('idinInitRequest', (): void => {
it('it should export idinInitRequest', (): void => {
jest.mock('firebase-admin', (): { [key: string]: any } => ({
initializeApp: () => ({
firestore: (): any => ({
settings: (): void => {},
}),
auth: (): void => { },
})
}));
myFunctions = require('../src/index');
const cFunction = require('../src/idinInitRequest');
assert.isObject(myFunctions);
assert.include(myFunctions, { idinInitRequest: cFunction });
});
it('idin issue', async (): Promise<void> => {
jest.mock('node-idin-beta', (): { [key: string]: any } => ({
getTransactionResponse: (): Promise<any> => Promise.reject('shiat'),
}));
jest.mock('firebase-admin', (): { [key: string]: any } => ({
initializeApp: () => ({
firestore: (): any => ({
settings: (): void => {},
}),
auth: (): void => { },
})
}));
myFunctions = require('../src/index');
const wrapped = testEnv.wrap(myFunctions.idinInitRequest);
const onCallObjects: [any, ContextOptions] = [
{ issuerId: 'issuer', merchantReturnUrl: 'url' },
{ auth: { uid: '32344' } },
];
await expect(wrapped(...onCallObjects)).rejects.toThrow('There was an error connecting to the idin issuer');
});
});
});
If you have any other approach to solve this, be welcome to share it.