Search code examples
unit-testingjestjsgoogle-cloud-functionssinon

Sinon stub require multiple times


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');
    });
  });
});

Solution

  • 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.