【问题标题】:How do you unit test a firebase function wrapped with express?如何对用 express 包装的 firebase 函数进行单元测试?
【发布时间】:2018-08-27 08:51:54
【问题描述】:

借助 firebase 函数,您可以利用 express 来实现不错的功能,例如中间件等。我使用 this example 获得了如何编写 https firebase 函数的灵感,由 express 提供支持。

但是,我的问题是 the official firebase documentation on how to do unit testing 不包含 https-express 示例。

所以我的问题是如何单元测试以下功能(打字稿)?:

// omitted init. of functions
import * as express from 'express';

const cors = require('cors')({origin: true});
const app = express();
app.use(cors);

// The function to test
app.get('helloWorld, (req, res) => {
   res.send('hello world');
   return 'success';
});

exports.app = functions.https.onRequest(app);

【问题讨论】:

  • 你要在线测试还是离线测试?
  • 我认为离线是首选。

标签: firebase google-cloud-functions


【解决方案1】:

我已经使用 firebase-functions-testnode-mocks-http 完成了这项工作。

我有这个实用程序类 FunctionCaller.js :

    'use strict';
    
    var httpMocks       = require('node-mocks-http');
    var eventEmitter    = require('events').EventEmitter;
    
    const FunctionCaller = class {
    
      constructor(aYourFunctionsIndex) {
        this.functions_index = aYourFunctionsIndex;
      }
    
      async postFunction(aFunctionName,aBody,aHeaders,aCookies) {
    
        let url = (aFunctionName[0]=='/') ? aFunctionName : `/${aFunctionName}`;
        let options = {
          method: 'POST',
          url: url,
          body: aBody
        };
        if (aHeaders)
          options.headers = aHeaders;
    
        if (aCookies) {
          options.cookies = {};
          for (let k in aCookies) {
            let v = aCookies[k];
            if (typeof(v)=='string') {
              options.cookies[k] = {value: v};
            } else if (typeof(v)=='object') {
              options.cookies[k] = v;
            }
          }
        }
    
        var request = httpMocks.createRequest(options);
        var response = httpMocks.createResponse({eventEmitter: eventEmitter});
    
    
        var me = this;
        await new Promise(function(resolve){
          response.on('end', resolve);
          if (me.functions_index[aFunctionName])
            me.functions_index[aFunctionName](request, response);
          else
            me.functions_index.app(request, response);
        });
        return response;
      }
    
      async postObject(aFunctionName,aBody,aHeaders,aCookies) {
        let response = await this.postFunction(aFunctionName,aBody,aHeaders,aCookies);
        return JSON.parse(response._getData());
      }
    
      async getFunction(aFunctionName,aParams,aHeaders,aCookies) {
        let url = (aFunctionName[0]=='/') ? aFunctionName : `/${aFunctionName}`;
        let options = {
          method: 'GET',
          url: url,
          query: aParams   // guessing here
        };
        if (aHeaders)
          options.headers = aHeaders;
    
        if (aCookies) {
          options.cookies = {};
          for (let k in aCookies) {
            let v = aCookies[k];
            if (typeof(v)=='string') {
              options.cookies[k] = {value: v};
            } else if (typeof(v)=='object') {
              options.cookies[k] = v;
            }
          }
        }
    
        var request = httpMocks.createRequest(options);
        var response = httpMocks.createResponse({eventEmitter: eventEmitter});
    
        var me = this;
        await new Promise(function(resolve){
          response.on('end', resolve);
          if (me.functions_index[aFunctionName])
            me.functions_index[aFunctionName](request, response);
          else
            me.functions_index.app(request, response);
        });
        return response;
      }
    
      async getObject(aFunctionName,aParams,aHeaders,aCookies) {
        let response = await this.getFunction(aFunctionName,aParams,aHeaders,aCookies);
        return JSON.parse(response._getData());
      }
    
    };
    
    module.exports = FunctionCaller;

我的应用被挂载为应用:

exports.app = functions.https.onRequest(expressApp);

我的 firebase.json 包含:

"rewrites": [
    :
    :
    :
    {
    "source": "/path/to/function", "function": "app"
    }
]

在我顶部的测试文件中:

const FunctionCaller = require('../FunctionCaller');
let fire_functions = require('../index');
const fnCaller = new FunctionCaller(fire_functions);

然后在我做的测试中:

let response = await fnCaller.postFunction('/path/to/function',anObject);

它使用 anObject 作为 request.body 调用我的函数并返回响应对象。

我在 Firebase 上使用节点 8 来获取异步/等待等。

【讨论】:

  • 谢谢,但我收到了TypeError: Cannot read property 'pipesCount' of undefined
【解决方案2】:

这适用于 Jest

import supertest from 'supertest'
import test from 'firebase-functions-test'
import sinon from 'sinon'
import admin from 'firebase-admin'

let undertest, adminInitStub, request
const functionsTest = test()

beforeAll(() => {
  adminInitStub = sinon.stub(admin, 'initializeApp')
  undertest = require('../index')
  // inject with the exports.app methode from the index.js
  request = supertest(undertest.app)
})

afterAll(() => {
  adminInitStub.restore()
  functionsTest.cleanup()
})

it('get app', async () => {
  let actual = await request.get('/')
  let { ok, status, body } = actual
  expect(ok).toBe(true)
  expect(status).toBeGreaterThanOrEqual(200)
  expect(body).toBeDefined()
})

【讨论】:

  • 你指的这个exports.app方法是什么?这通常不会在 firebase 函数索引文件中找到。
  • 它是索引文件中的导出函数。例如,索引文件中的“exports.api = functions.https.onRequest(app)”将位于测试文件“request = supertest(undertest.api)”中。所以每个导出的函数都可以用 'request = supertest(undertest.function_name)' 进行测试
【解决方案3】:

测试是为了建立信心或信任。

我将从对 FireBase 中的功能进行单元测试开始。如果没有定义更多要求,我会遵循文档。一旦这些单元测试通过,您就可以考虑在 Express 级别需要哪种类型的测试。请记住,您已经测试过该功能,在 Express 级别测试的唯一内容是映射是否正确。该级别的一些测试应该足以确保映射不会由于某些更改而变得“陈旧”。

如果你想在不涉及数据库的情况下测试 Express 及以上级别,那么你会考虑一个模拟框架来为你充当数据库。

希望这可以帮助您考虑需要进行哪些测试。

【讨论】:

    【解决方案4】:

    对于本地和无网络单元测试,您可以将 app.get("helloWorld", ...) 回调重构为单独的函数并使用模拟对象调用它。

    一般的方法是这样的:

    main.js:

    // in the Firebase code:
    export function helloWorld(req, res) { res.send(200); }
    app.get('helloWorld', helloWorld);
    

    main.spec.js:使用 jasmine & sinon

    // in the test:
    import { helloWorld } from './main.js';
    import sinon from 'sinon';
    const reqMock = {};
    const resMock = { send: sinon.spy() }; 
    
    it('always responds with 200', (done) => {
        helloWorld(reqMock, resMock);
        expect(resMock.send.callCount).toBe(1);
        expect(resMock.send).toHaveBeenCalledWith(200);
    });
    

    【讨论】:

      【解决方案5】:

      您可以将 supertest 与 Firebase 中的 guide 配对使用。下面是一个测试您的应用的非常基本的示例,但是,您可以通过集成 mocha 使其更复杂/更好。

      import * as admin from 'firebase-admin'
      import * as testFn from 'firebase-functions-test'
      import * as sinon from 'sinon'
      import * as request from 'supertest'
      const test = testFn()
      import * as myFunctions from './get-tested' // relative path to functions code
      const adminInitStub = sinon.stub(admin, 'initializeApp')
      
      request(myFunctions.app)
        .get('/helloWorld')
        .expect('hello world')
        .expect(200)
        .end((err, res) => {
          if (err) {
            throw err
          }
        })
      

      【讨论】:

        【解决方案6】:

        mock-express 这样的东西适合你吗?它应该允许您测试路径,而无需实际强制您创建快速服务器。

        https://www.npmjs.com/package/mock-express

        【讨论】:

          【解决方案7】:

          您可以使用postman 应用程序进行单元测试。 使用您的项目名称输入以下网址

          https://us-central1-your-project.cloudfunctions.net/hello

          app.get('/hello/',(req, res) => {
             res.send('hello world');
             return 'success';
          });
          

          【讨论】:

          • 嗨沙拉特,感谢您的回答。确实我可以用邮递员进行测试,但答案是专门要求进行单元测试。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-02-08
          • 2018-04-02
          • 1970-01-01
          相关资源
          最近更新 更多