本頁介紹了為您的功能編寫單元測試的最佳實踐和工具,例如作為持續整合 (CI) 系統一部分的測試。為了讓測試更容易,Firebase 提供了適用於 Cloud Functions 的 Firebase Test SDK。它作為firebase-functions-test
發佈在 npm 上,並且是firebase-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 控制台的服務帳號窗格。
- 選擇 App Engine 預設服務帳戶,然後使用右側的選項選單選擇Create key 。
- 出現提示時,選擇 JSON 作為金鑰類型,然後按一下Create 。
儲存金鑰檔案後,初始化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
有 2 個參數:
- data (必需):傳送到
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等函式庫來做出這些斷言。
在線模式下斷言
如果您在線上模式下初始化了 Firebase Test SDK for Cloud Functions,則可以使用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 spies來斷言某些方法已被調用,並帶有您期望的參數。
測試 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 參考。