使用 Jasmine 和 Typescript 测试 Firebase 的云功能
Testing Cloud Functions for Firebase with Jasmine and Typescript
我目前正在研究 Google Cloud Functions 并且有一些用 typescript 编写的基本测试函数。
函数按预期工作,我现在正尝试使用 Jasmine 创建单元测试。 (我没有按照文档使用 Chai/sinon,因为我项目的其余部分使用 jasmine)。
我有两个问题
1) 由于这个错误
,测试没有运行
throw new Error('Firebase config variables are not available. ' +
^
Error: Firebase config variables are not available. Please use the
latest version of the Firebase CLI to deploy this function
2) 鉴于测试确实 运行,我不确定如何测试响应是否符合预期。
索引文件
import * as functions from 'firebase-functions'
import { helloWorldHandler } from './functions/hello-world';
export let helloWorld = functions.https.onRequest((req, res) => {
helloWorldHandler(req, res);
});
正在测试的文件
export let helloWorldHandler = (request, response) => {
response.send("Hello from Firebase Cloud!");
}
规格
import {} from 'jasmine';
import * as functions from 'firebase-functions'
import { helloWorldHandler } from './hello-world';
import * as endpoints from '../index';
describe('Cloud Functions : Hello World', () => {
let configStub = {
firebase: {
databaseURL: "https://myProject.firebaseio.com",
storageBucket: "myProject.appspot.com",
}
};
it('should return correct message', () => {
let spy = spyOn(functions, 'config').and.returnValue(configStub);
const expected = 'Hello from Firebase Cloud!';
// A fake request and response objects
const req : any = {};
const res : any = { };
endpoints.helloWorld(req, res);
//here test response from helloWorld is as expected
});
});
如果您正在编写单元测试,那么您不想测试第三方 API。因此,目标应该是隔离您的代码逻辑并对其进行测试。端到端测试最适合对集成进行回归测试。
所以这里的第一步是从图片中删除 firebase-functions
和数据库 SDK 等工具(尽可能合理)。我通过将我的库与函数逻辑分开来实现这一点,如下所示:
// functions/lib/http.js
exports.httpFunction = (req, res) => {
res.send(`Hello ${req.data.foo}`);
};
// functions/index.js
const http = require('lib/http');
const functions = require('firebase-functions');
// we have decoupled the Functions invocation from the method
// so the method can be tested without including the functions lib!
functions.https.onRequest(http.httpFunction);
现在我有了可以通过单元测试测试的很好的隔离逻辑。我模拟了将传递到我的方法中的任何参数,从图片中删除了第三方 API。
所以这是我在 Jasmine 中的单元测试的样子:
// spec/lib/http.spec.js
const http = require('../functions/lib/http');
describe('functions/lib/http', () => {
expect('send to be called with "hello world"', () => {
// first thing to do is mock req and res objects
const req = {data: {foo: 'world'}};
const res = {send: (s) => {});
// now let's monitor res.send to make sure it gets called
spyOn(res, 'send').and.callThrough();
// now run it
http.httpFunction(req, res);
// new test it
expect(res.send).toHaveBeenCalledWith("Hello world");
});
});
测试第三方库有很多复杂性。此处的最佳答案是尽早应用 TDD/BDD 原则并将第三方库抽象为可以轻松模拟的服务。
例如,如果我在我的函数中与 Firebase Admin 进行交互,我很容易最终得到一个有很多第三方依赖项需要应对的方法:
// functions/lib/http.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const env = require('./env');
const serviceAccount = require(env.serviceAccountPath);
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: `https://${env.dbUrl}.firebaseio.com`
});
exports.httpFunction = (req, res) => {
let path = null;
let data = null;
// this is what I really want to test--my logic!
if( req.query.foo ) {
path = 'foo';
data = 1;
}
// but there's this third library party coupling :(
if( path !== null ) {
let ref = admin.database.ref().child(path);
return ref.set(data)
.then(() => res.send('done'))
.catch(e => res.status(500).send(e));
}
else {
res.status(500).send('invalid query');
}
};
要测试此示例,我必须包含并初始化 Functions 以及 Firebase Admin SDK,否则我必须找到一种方法来模拟这些服务。所有这些看起来都是一项相当大的工作。相反,我可以有一个 DataStore 抽象并利用它:
// An interface for the DataStore abstraction
// This is where my Firebase logic would go, neatly packaged
// and decoupled
class DataStore {
set: (path, data) => {
// This is the home for admin.database.ref(path).set(data);
}
}
// An interface for the HTTPS abstraction
class ResponseHandler {
success: (message) => { /* res.send(message); */ }
fail: (error) => { /* res.status(500).send(error); */ }
}
如果我现在添加从 Functions 过程中抽象出我的逻辑的第一个原则,那么我的布局如下所示:
// functions/lib/http.js
exports.httpFunction = (query, responseHandler, dataStore) => {
if( query.foo ) {
return dataStore.set('foo', 1)
.then(() => responseHandler.success())
.catch(e => responseHandler.fail(e));
}
else {
responseHandler.fail('invalid query');
}
};
允许我编写一个更优雅的单元测试:
// spec/lib/http
describe('functions/lib/http', () => {
expect('is successful if "foo" parameter is passed', () => {
// first thing to do is mock req and res objects
const query = {foo: 'bar'};
const responseHandler = {success: () => {}, fail: () => {});
const dataStore = {set: () => {return Promise.resolve()}};
// now let's monitor the results
spyOn(responseHandler, 'success');
// now run it
http.httpFunction(query, responseHandler, dataStore);
// new test it
expect(res.success).toHaveBeenCalled();
});
});
我的代码的其余部分也不错:
// functions/lib/firebase.datastore.js
// A centralized place for our third party lib!
// Less mocking and e2e testing!
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const serviceAccount = require(env.serviceAccountPath);
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: `https://${env.dbUrl}.firebaseio.com`
});
exports.set = (path, data) => {
return admin.database.ref(path).set(data);
};
// functions/index.js
const functions = require('firebase-functions');
const dataStore = require('./lib/firebase.datastore');
const ResponseHandler = require('./lib/express.responseHandler');
const env = require('./env');
const http = require('./lib/http');
dataStore.initialize(env);
exports.httpFunction = (req, res) => {
const handler = new ResponseHandler(res);
return http.httpFunction(req.query, handler, dataStore);
};
更不用说从良好的 BDD 心态开始,我还以模块化的方式很好地隔离了我的项目的组件,当我们在第 2 阶段发现所有范围蔓延时,这会很好。: )
我目前正在研究 Google Cloud Functions 并且有一些用 typescript 编写的基本测试函数。
函数按预期工作,我现在正尝试使用 Jasmine 创建单元测试。 (我没有按照文档使用 Chai/sinon,因为我项目的其余部分使用 jasmine)。
我有两个问题 1) 由于这个错误
,测试没有运行throw new Error('Firebase config variables are not available. ' + ^ Error: Firebase config variables are not available. Please use the latest version of the Firebase CLI to deploy this function
2) 鉴于测试确实 运行,我不确定如何测试响应是否符合预期。
索引文件
import * as functions from 'firebase-functions'
import { helloWorldHandler } from './functions/hello-world';
export let helloWorld = functions.https.onRequest((req, res) => {
helloWorldHandler(req, res);
});
正在测试的文件
export let helloWorldHandler = (request, response) => {
response.send("Hello from Firebase Cloud!");
}
规格
import {} from 'jasmine';
import * as functions from 'firebase-functions'
import { helloWorldHandler } from './hello-world';
import * as endpoints from '../index';
describe('Cloud Functions : Hello World', () => {
let configStub = {
firebase: {
databaseURL: "https://myProject.firebaseio.com",
storageBucket: "myProject.appspot.com",
}
};
it('should return correct message', () => {
let spy = spyOn(functions, 'config').and.returnValue(configStub);
const expected = 'Hello from Firebase Cloud!';
// A fake request and response objects
const req : any = {};
const res : any = { };
endpoints.helloWorld(req, res);
//here test response from helloWorld is as expected
});
});
如果您正在编写单元测试,那么您不想测试第三方 API。因此,目标应该是隔离您的代码逻辑并对其进行测试。端到端测试最适合对集成进行回归测试。
所以这里的第一步是从图片中删除 firebase-functions
和数据库 SDK 等工具(尽可能合理)。我通过将我的库与函数逻辑分开来实现这一点,如下所示:
// functions/lib/http.js
exports.httpFunction = (req, res) => {
res.send(`Hello ${req.data.foo}`);
};
// functions/index.js
const http = require('lib/http');
const functions = require('firebase-functions');
// we have decoupled the Functions invocation from the method
// so the method can be tested without including the functions lib!
functions.https.onRequest(http.httpFunction);
现在我有了可以通过单元测试测试的很好的隔离逻辑。我模拟了将传递到我的方法中的任何参数,从图片中删除了第三方 API。
所以这是我在 Jasmine 中的单元测试的样子:
// spec/lib/http.spec.js
const http = require('../functions/lib/http');
describe('functions/lib/http', () => {
expect('send to be called with "hello world"', () => {
// first thing to do is mock req and res objects
const req = {data: {foo: 'world'}};
const res = {send: (s) => {});
// now let's monitor res.send to make sure it gets called
spyOn(res, 'send').and.callThrough();
// now run it
http.httpFunction(req, res);
// new test it
expect(res.send).toHaveBeenCalledWith("Hello world");
});
});
测试第三方库有很多复杂性。此处的最佳答案是尽早应用 TDD/BDD 原则并将第三方库抽象为可以轻松模拟的服务。
例如,如果我在我的函数中与 Firebase Admin 进行交互,我很容易最终得到一个有很多第三方依赖项需要应对的方法:
// functions/lib/http.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const env = require('./env');
const serviceAccount = require(env.serviceAccountPath);
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: `https://${env.dbUrl}.firebaseio.com`
});
exports.httpFunction = (req, res) => {
let path = null;
let data = null;
// this is what I really want to test--my logic!
if( req.query.foo ) {
path = 'foo';
data = 1;
}
// but there's this third library party coupling :(
if( path !== null ) {
let ref = admin.database.ref().child(path);
return ref.set(data)
.then(() => res.send('done'))
.catch(e => res.status(500).send(e));
}
else {
res.status(500).send('invalid query');
}
};
要测试此示例,我必须包含并初始化 Functions 以及 Firebase Admin SDK,否则我必须找到一种方法来模拟这些服务。所有这些看起来都是一项相当大的工作。相反,我可以有一个 DataStore 抽象并利用它:
// An interface for the DataStore abstraction
// This is where my Firebase logic would go, neatly packaged
// and decoupled
class DataStore {
set: (path, data) => {
// This is the home for admin.database.ref(path).set(data);
}
}
// An interface for the HTTPS abstraction
class ResponseHandler {
success: (message) => { /* res.send(message); */ }
fail: (error) => { /* res.status(500).send(error); */ }
}
如果我现在添加从 Functions 过程中抽象出我的逻辑的第一个原则,那么我的布局如下所示:
// functions/lib/http.js
exports.httpFunction = (query, responseHandler, dataStore) => {
if( query.foo ) {
return dataStore.set('foo', 1)
.then(() => responseHandler.success())
.catch(e => responseHandler.fail(e));
}
else {
responseHandler.fail('invalid query');
}
};
允许我编写一个更优雅的单元测试:
// spec/lib/http
describe('functions/lib/http', () => {
expect('is successful if "foo" parameter is passed', () => {
// first thing to do is mock req and res objects
const query = {foo: 'bar'};
const responseHandler = {success: () => {}, fail: () => {});
const dataStore = {set: () => {return Promise.resolve()}};
// now let's monitor the results
spyOn(responseHandler, 'success');
// now run it
http.httpFunction(query, responseHandler, dataStore);
// new test it
expect(res.success).toHaveBeenCalled();
});
});
我的代码的其余部分也不错:
// functions/lib/firebase.datastore.js
// A centralized place for our third party lib!
// Less mocking and e2e testing!
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const serviceAccount = require(env.serviceAccountPath);
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: `https://${env.dbUrl}.firebaseio.com`
});
exports.set = (path, data) => {
return admin.database.ref(path).set(data);
};
// functions/index.js
const functions = require('firebase-functions');
const dataStore = require('./lib/firebase.datastore');
const ResponseHandler = require('./lib/express.responseHandler');
const env = require('./env');
const http = require('./lib/http');
dataStore.initialize(env);
exports.httpFunction = (req, res) => {
const handler = new ResponseHandler(res);
return http.httpFunction(req.query, handler, dataStore);
};
更不用说从良好的 BDD 心态开始,我还以模块化的方式很好地隔离了我的项目的组件,当我们在第 2 阶段发现所有范围蔓延时,这会很好。: )