firebase / firebase-functions-test

MIT License
232 stars 48 forks source link

Async test does not wait for await in https.onRequest triggered function #43

Closed noelmansour closed 5 years ago

noelmansour commented 5 years ago

Version info

firebase-functions-test: ^0.1.6

firebase-functions: ^3.0.2

firebase-admin: ^8.2.0

Test case

functions/test/index.test.ts:

import * as sinon from 'sinon';
import * as admin from "firebase-admin";
import * as chai from 'chai';

const test = require('firebase-functions-test')({
    projectId: 'project-id',
});

describe('Cloud Functions', () => {
    let functions: any;

    before(() => {
        admin.initializeApp();
        functions = require('../src/debug');
    });

    after(() => {
        test.cleanup();
    });

    afterEach(() => {
        sinon.restore();
    });

    describe('callBar', () => {
        it('should call bar without error', async () => {
            console.log('test: about to await');

            await test.wrap(functions.bar)({}, {});

            console.log('test: done await');
        })
    });

    describe('callFoo', () => {
        it('should call foo without error', async () => {
            const sendStub = sinon.stub();

            console.log('test: about to await');
            await functions.foo({}, {
                send: sendStub,
            });
            console.log('test: done await');

            chai.assert(sendStub.calledOnceWith(200));
        })
    });

});

functions/src/debug.ts:

import * as functions from 'firebase-functions';

export const foo = functions.https.onRequest(async (req, resp) => {
    await delayedPromise();
    resp.send(200);
});

export const bar = functions.https.onCall(async (data, context) => {
    await delayedPromise();
});

async function delayedPromise() {
    console.log('https: about to await');
    await new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve();
        }, 500);
    });
    console.log('https: done await');
}

Steps to reproduce

Run the tests. I'm using the npm run test script from my package.json:

{
  "scripts": {
    "test": "mocha --require ts-node/register test/**/*.ts"
  }
}

Expected behavior

The test should await until all awaits from the function are done (for all function trigger types).

Actual behavior

The test does not wait for the await call from the https.onRequest trigger. This is the output from running the tests.

For the https.onRequest triggered function, the https: done await appears after the test: done await (not expected). However, for the https.onCall triggered function, the https: done await log appears before the test: done await (expected).

  Cloud Functions
    callBar
test: about to await
https: about to await
https: done await
test: done await
      ✓ should call bar without error (504ms)
    callFoo
test: about to await
https: about to await
test: done await
      1) should call foo without error

  1 passing (785ms)
  1 failing

  1) Cloud Functions
       callFoo
         should call foo without error:
     AssertionError: Unspecified AssertionError
      at Object.<anonymous> (test/index.test.ts:45:18)
      at Generator.next (<anonymous>)
      at fulfilled (test/index.test.ts:4:58)
      at <anonymous>
      at process._tickCallback (internal/process/next_tick.js:189:7)

https: done await
noelmansour commented 5 years ago

After some further investigation I've managed to find a workaround.

Looking at the function declarations for both:

export declare function onRequest(handler: (req: Request, resp: express.Response) => void): HttpsFunction;
export declare function onCall(handler: (data: any, context: CallableContext) => any | Promise<any>): HttpsFunction & Runnable<any>;

I can see that onCall returns a Promise whereas onRequest returns void. This would explain the behavior noted above.

In fact, if the onRequest implementation uses promise/then (no async/await) the same issue can be observed. i.e.

export const foo = functions.https.onRequest((req, resp) => {
    delayedPromise().then(() => resp.send(200));
});

Also, according to https://firebase.google.com/docs/functions/unit-testing#testing_http_functions, "Testing of HTTP Callable Functions is not yet supported" so using a https.onCall triggered function is not a very useful comparison.

One approach is to call done() in the Response.send() mocked implementation. Alternatively, supertest can be used as per https://cloud.google.com/functions/docs/bestpractices/testing#integration_tests_2