本页介绍了为您的功能编写单元测试的最佳实践和工具,例如将成为持续集成 (CI) 系统一部分的测试。为了简化测试,Firebase 提供了适用于 Cloud Functions 的 Firebase Test SDK。它作为firebase-functions-test
在 npm 上分发,并且是firebase-functions
-functions 的配套测试 SDK。用于 Cloud Functions 的 Firebase 测试 SDK:
- 负责为您的测试进行适当的设置和拆卸,例如设置和取消设置
firebase-functions
所需的环境变量。 - 生成示例数据和事件上下文,以便您只需指定与测试相关的字段。
测试设置
通过在函数文件夹中运行以下命令来安装firebase-functions-test
和测试框架Mocha :
npm install --save-dev firebase-functions-test
npm install --save-dev mocha
接下来在 functions 文件夹中创建一个test
文件夹,在其中为您的测试代码创建一个新文件,并将其命名为index.test.js
之类的名称。
最后,修改functions/package.json
添加以下内容:
"scripts": {
"test": "mocha --reporter spec"
}
编写测试后,您可以通过在函数目录中运行npm test
来运行它们。
为 Cloud Functions 初始化 Firebase 测试 SDK
有两种使用firebase-functions-test
方法:
- 在线模式(推荐):编写与专用于测试的 Firebase 项目交互的测试,以便数据库写入、用户创建等实际发生,并且您的测试代码可以检查结果。这也意味着您的函数中使用的其他 Google SDK 也可以正常工作。
- 离线模式:编写没有副作用的孤立和离线单元测试。这意味着任何与 Firebase 产品交互的方法调用(例如写入数据库或创建用户)都需要存根。如果您有 Cloud Firestore 或实时数据库功能,通常不建议使用离线模式,因为它会大大增加您的测试代码的复杂性。
在线模式初始化SDK(推荐)
如果您想编写与测试项目交互的测试,则需要提供通过firebase-admin
初始化应用程序所需的项目配置值,以及服务帐户密钥文件的路径。
要获取 Firebase 项目的配置值:
- 在Firebase 控制台中打开您的项目设置。
- 在您的应用中,选择所需的应用。
在右侧窗格中,选择下载 Apple 和 Android 应用程序配置文件的选项。
对于 Web 应用程序,选择配置以显示配置值。
要创建密钥文件:
- 打开 Google Cloud Console 的服务帐户面板。
- 选择 App Engine 默认服务帐户,然后使用右侧的选项菜单选择Create key 。
- 出现提示时,选择 JSON 作为密钥类型,然后单击创建。
保存密钥文件后,初始化SDK:
// At the top of test/index.test.js
const test = require('firebase-functions-test')({
databaseURL: 'https://my-project.firebaseio.com',
storageBucket: 'my-project.appspot.com',
projectId: 'my-project',
}, 'path/to/serviceAccountKey.json');
离线模式初始化SDK
如果您想编写完全离线的测试,您可以不带任何参数初始化 SDK:
// At the top of test/index.test.js
const test = require('firebase-functions-test')();
模拟配置值
如果您在函数代码中使用functions.config()
,则可以模拟配置值。例如,如果functions/index.js
包含以下代码:
const functions = require('firebase-functions');
const key = functions.config().stripe.key;
然后你可以像这样模拟你的测试文件中的值:
// Mock functions config values
test.mockConfig({ stripe: { key: '23wr42ewr34' }});
导入你的函数
要导入您的函数,请使用require
将您的主要函数文件作为模块导入。请确保仅在初始化firebase-functions-test
和模拟配置值后执行此操作。
// after firebase-functions-test has been initialized
const myFunctions = require('../index.js'); // relative path to functions code
如果您在离线模式下初始化了firebase-functions-test
,并且您的函数代码中有admin.initializeApp()
,那么您需要在导入函数之前对其进行存根:
// 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'); // Now we can require index.js and save the exports inside a namespace called myFunctions. myFunctions = require('../index');
测试后台(非 HTTP)功能
测试非 HTTP 功能的过程包括以下步骤:
- 用
test.wrap
方法包装你想测试的函数 - 构建测试数据
- 使用您构建的测试数据和您想要指定的任何事件上下文字段调用包装函数。
- 对行为做出断言。
首先包装您要测试的功能。假设你在functions/index.js
中有一个名为makeUppercase
的函数,你想测试它。在functions/test/index.test.js
中写入如下内容
// "Wrap" the makeUpperCase function from index.js
const myFunctions = require('../index.js');
const wrapped = test.wrap(myFunctions.makeUppercase);
wrapped
是一个在调用时调用makeUppercase
的函数。 wrapped
有两个参数:
- 数据(必需):发送到
makeUppercase
的数据。这直接对应于发送到您编写的函数处理程序的第一个参数。firebase-functions-test
提供了构建自定义数据或示例数据的方法。 - eventContextOptions (可选):您要指定的事件上下文的字段。事件上下文是发送到您编写的函数处理程序的第二个参数。如果您在调用
wrapped
时不包含eventContextOptions
参数,则仍会生成带有合理字段的事件上下文。您可以通过在此处指定来覆盖某些生成的字段。请注意,您只需包含要覆盖的字段。将生成您未覆盖的任何字段。
const data = … // See next section for constructing test data
// Invoke the wrapped function without specifying the event context.
wrapped(data);
// Invoke the function, and specify params
wrapped(data, {
params: {
pushId: '234234'
}
});
// Invoke the function, and specify auth and auth Type (for real time database functions only)
wrapped(data, {
auth: {
uid: 'jckS2Q0'
},
authType: 'USER'
});
// Invoke the function, and specify all the fields that can be specified
wrapped(data, {
eventId: 'abc',
timestamp: '2018-03-23T17:27:17.099Z',
params: {
pushId: '234234'
},
auth: {
uid: 'jckS2Q0' // only for real time database functions
},
authType: 'USER' // only for real time database functions
});
构建测试数据
包装函数的第一个参数是用于调用底层函数的测试数据。有许多方法可以构建测试数据。
使用自定义数据
firebase-functions-test
有许多函数可用于构建测试函数所需的数据。例如,使用test.firestore.makeDocumentSnapshot
创建 Firestore DocumentSnapshot
。第一个参数是数据,第二个参数是完整的引用路径,还有一个可选的第三个参数用于您可以指定的快照的其他属性。
// Make snapshot
const snap = test.firestore.makeDocumentSnapshot({foo: 'bar'}, 'document/path');
// Call wrapped function with the snapshot
const wrapped = test.wrap(myFunctions.myFirestoreDeleteFunction);
wrapped(snap);
如果您正在测试onUpdate
或onWrite
函数,则需要创建两个快照:一个用于之前状态,一个用于之后状态。然后,您可以使用makeChange
方法使用这些快照创建Change
对象。
// Make snapshot for state of database beforehand
const beforeSnap = test.firestore.makeDocumentSnapshot({foo: 'bar'}, 'document/path');
// Make snapshot for state of database after the change
const afterSnap = test.firestore.makeDocumentSnapshot({foo: 'faz'}, 'document/path');
const change = test.makeChange(beforeSnap, afterSnap);
// Call wrapped function with the Change object
const wrapped = test.wrap(myFunctions.myFirestoreUpdateFunction);
wrapped(change);
有关所有其他数据类型的类似功能,请参阅API 参考。
使用示例数据
如果您不需要自定义测试中使用的数据,那么firebase-functions-test
提供了为每种函数类型生成示例数据的方法。
// For Firestore onCreate or onDelete functions
const snap = test.firestore.exampleDocumentSnapshot();
// For Firestore onUpdate or onWrite functions
const change = test.firestore.exampleDocumentSnapshotChange();
有关获取每种函数类型的示例数据的方法,请参阅API 参考。
使用存根数据(用于离线模式)
如果您在离线模式下初始化了 SDK,并且正在测试 Cloud Firestore 或实时数据库功能,则应使用带有存根的普通对象,而不是创建实际的DocumentSnapshot
或DataSnapshot
。
假设您正在为以下函数编写单元测试:
// Listens for new messages added to /messages/:pushId/original and creates an // uppercase version of the message to /messages/:pushId/uppercase exports.makeUppercase = functions.database.ref('/messages/{pushId}/original') .onCreate((snapshot, context) => { // Grab the current value of what was written to the Realtime Database. const original = snapshot.val(); functions.logger.log('Uppercasing', context.params.pushId, original); const uppercase = original.toUpperCase(); // You must return a Promise when performing asynchronous tasks inside a Functions such as // writing to the Firebase Realtime Database. // Setting an "uppercase" sibling in the Realtime Database returns a Promise. return snapshot.ref.parent.child('uppercase').set(uppercase); });
在函数内部, snap
使用了两次:
-
snap.val()
-
snap.ref.parent.child('uppercase').set(uppercase)
在测试代码中,创建一个普通对象,这两个代码路径都可以在其中工作,并使用Sinon来存根这些方法。
// The following lines creates a fake snapshot, 'snap', which returns 'input' when snap.val() is called, // and returns true when snap.ref.parent.child('uppercase').set('INPUT') is called. const snap = { val: () => 'input', ref: { parent: { child: childStub, } } }; childStub.withArgs(childParam).returns({ set: setStub }); setStub.withArgs(setParam).returns(true);
做出断言
在初始化 SDK、包装函数和构造数据后,您可以使用构造的数据调用包装函数并对行为进行断言。您可以使用诸如Chai之类的库来进行这些断言。
在在线模式下进行断言
如果您在在线模式下初始化了适用于 Cloud Functions 的 Firebase 测试 SDK,则可以使用firebase-admin
SDK 断言所需的操作(例如数据库写入)已经发生。
下面的示例断言“INPUT”已写入测试项目的数据库。
// Create a DataSnapshot with the value 'input' and the reference path 'messages/11111/original'. const snap = test.database.makeDataSnapshot('input', 'messages/11111/original'); // Wrap the makeUppercase function const wrapped = test.wrap(myFunctions.makeUppercase); // Call the wrapped function with the snapshot you constructed. return wrapped(snap).then(() => { // Read the value of the data at messages/11111/uppercase. Because `admin.initializeApp()` is // called in functions/index.js, there's already a Firebase app initialized. Otherwise, add // `admin.initializeApp()` before this line. return admin.database().ref('messages/11111/uppercase').once('value').then((createdSnap) => { // Assert that the value is the uppercased version of our input. assert.equal(createdSnap.val(), 'INPUT'); }); });
在离线模式下进行断言
您可以对函数的预期返回值进行断言:
const childParam = 'uppercase'; const setParam = 'INPUT'; // Stubs are objects that fake and/or record function calls. // These are excellent for verifying that functions have been called and to validate the // parameters passed to those functions. const childStub = sinon.stub(); const setStub = sinon.stub(); // The following lines creates a fake snapshot, 'snap', which returns 'input' when snap.val() is called, // and returns true when snap.ref.parent.child('uppercase').set('INPUT') is called. const snap = { val: () => 'input', ref: { parent: { child: childStub, } } }; childStub.withArgs(childParam).returns({ set: setStub }); setStub.withArgs(setParam).returns(true); // Wrap the makeUppercase function. const wrapped = test.wrap(myFunctions.makeUppercase); // Since we've stubbed snap.ref.parent.child(childParam).set(setParam) to return true if it was // called with the parameters we expect, we assert that it indeed returned true. return assert.equal(wrapped(snap), true);
您还可以使用Sinon 间谍来断言某些方法已被调用,并且具有您期望的参数。
测试 HTTP 功能
要测试 HTTP onCall 函数,请使用与测试后台函数相同的方法。
如果您正在测试 HTTP onRequest 函数,则应在以下情况下使用firebase-functions-test
:
- 你使用
functions.config()
- 您的函数与 Firebase 项目或其他 Google API 交互,并且您希望使用真实的 Firebase 项目及其凭据进行测试。
HTTP onRequest 函数有两个参数:一个请求对象和一个响应对象。以下是您可以如何测试addMessage()
示例函数:
- 覆盖响应对象中的重定向函数,因为
sendMessage()
调用它。 - 在重定向函数中,使用chai.assert来帮助断言应该使用哪些参数调用重定向函数:
// A fake request object, with req.query.text set to 'input' const req = { query: {text: 'input'} }; // A fake response object, with a stubbed redirect function which asserts that it is called // with parameters 303, 'new_ref'. const res = { redirect: (code, url) => { assert.equal(code, 303); assert.equal(url, 'new_ref'); done(); } }; // Invoke addMessage with our fake request and response objects. This will cause the // assertions in the response object to be evaluated. myFunctions.addMessage(req, res);
测试清理
在测试代码的最后,调用清理函数。这会取消设置 SDK 在初始化时设置的环境变量,并删除如果您使用 SDK 创建实时数据库DataSnapshot
或 Firestore DocumentSnapshot
可能已经创建的 Firebase 应用程序。
test.cleanup();
查看完整示例并了解更多信息
您可以在 Firebase GitHub 存储库中查看完整示例。
要了解更多信息,请参阅firebase-functions-test
的API 参考。