本頁面說明為函式編寫單元測試的最佳做法和工具,例如屬於持續整合 (CI) 系統的測試。為方便測試,Firebase 提供 Cloud Functions 的 Firebase Test SDK。這個 SDK 在 npm 上以 firebase-functions-test 的形式發布,是 firebase-functions 的輔助測試 SDK。Cloud Functions 的 Firebase Test SDK:
- 負責為測試進行適當的設定和拆解,例如設定及取消設定
firebase-functions所需的環境變數。 - 產生樣本資料和事件情境,因此您只需指定與測試相關的欄位。
測試設定
在函式資料夾中執行下列指令,安裝 firebase-functions-test 和測試架構 Jest:
npm install --save-dev firebase-functions-test
npm install --save-dev jest
接著,在 functions 資料夾中建立 test 資料夾,並在其中建立測試程式碼的新檔案,並命名為 index.test.js。
最後,修改 functions/package.json,新增下列內容:
"scripts": {
"test": "jest"
}
編寫測試後,您可以在函式目錄中執行 npm test 來執行測試。
正在為 Cloud Functions 初始化 Firebase Test SDK
firebase-functions-test 的使用方式有兩種:
- 線上模式 (建議):編寫與測試專用 Firebase 專案互動的測試,以便實際執行資料庫寫入、使用者建立等作業,並讓測試程式碼檢查結果。這也表示函式中使用的其他 Google SDK 也能正常運作。
- 離線模式:編寫獨立且離線的單元測試,不會產生副作用。也就是說,任何與 Firebase 產品互動的方法呼叫 (例如寫入資料庫或建立使用者),都必須進行虛設。如果您有 Cloud Firestore 或 Realtime Database 函式,通常不建議使用離線模式,因為這會大幅增加測試程式碼的複雜度。
在線上模式初始化 SDK (建議)
如要編寫與測試專案互動的測試,您必須提供透過 firebase-admin 初始化應用程式所需的 Firebase 設定值,以及服務帳戶金鑰檔案的路徑。
如要取得 Firebase 設定值,請按照下列步驟操作:
在 Firebase 控制台中,依序前往 「
Settings」(設定) > 「General」(一般) 頁面。捲動至「您的應用程式」資訊卡,然後選取所需應用程式。
取得 Firebase 設定:
如果是 Apple 和 Android 應用程式,請選取下載設定檔的選項。
如果是網頁應用程式,請選取「Config」,即可顯示設定值。
如何建立金鑰檔案:
在 Google Cloud 控制台中,前往「Service Accounts」(服務帳戶) 窗格。
選取 App Engine 預設服務帳戶,然後使用右側的選項選單選取「Create key」(建立金鑰)。
出現提示時,選取「JSON」做為金鑰類型,然後點選「建立」。
儲存金鑰檔案後,請初始化 SDK:
// At the top of test/index.test.js
// Make sure to use values from your actual Firebase configuration
const test = require('firebase-functions-test')({
databaseURL: 'https://PROJECT_ID.firebaseio.com',
storageBucket: 'PROJECT_ID.firebasestorage.app ',
projectId: 'PROJECT_ID',
}, '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/v1');
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(),則必須先虛設常式,再匯入函式。在測試檔案中,將 firebase-admin 設為 admin,然後將 initializeApp() 替換為 Jest 模擬:
// If initializeApp() is called in index.js, we mock it out before requiring index.js adminInitStub = jest.spyOn(admin, 'initializeApp').mockImplementation(() => {}); // 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 或 Realtime Database 函式,請使用含有虛設常式的純物件,而非建立實際的 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)
在測試程式碼中,建立一個一般物件,讓這兩個程式碼路徑都能運作,並使用 Jest 模擬函式來虛設方法。
// The following lines create 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, } } };
提出斷言
初始化 SDK、包裝函式並建構資料後,您可以使用建構的資料叫用包裝函式,並對行為進行斷言。您可以使用 Jest 的 expect 進行這些斷言。
在線上模式中提出聲明
如果您在線上模式中初始化 Cloud Functions 的 Firebase Test 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. return admin.database().ref('messages/11111/uppercase').once('value').then((createdSnap) => { // Assert that the value is the uppercased version of our input. expect(createdSnap.val()).toBe('INPUT'); }); });
在離線模式下進行斷言
您可以對函式的預期傳回值做出斷言:
const childParam = 'uppercase'; const setParam = 'INPUT'; // Spies/mocks 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 setStub = jest.fn().mockImplementation((val) => val === setParam ? true : undefined); const childStub = jest.fn().mockImplementation((path) => path === childParam ? { set: setStub } : undefined); // The following lines create 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, } } }; // Wrap the makeUppercase function. const wrapped = test.wrap(myFunctions.makeUppercase); // Since we've mocked 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 wrapped(snap).then(makeUppercaseResult => { expect(makeUppercaseResult).toBe(true); });
您也可以使用 Jest 模擬函式,確認特定方法是否已呼叫,以及是否使用預期的參數。
測試 HTTP 函式
如要測試 HTTP onCall 函式,請採用與測試背景函式相同的方法。
如要測試 HTTP onRequest 函式,請在下列情況使用 firebase-functions-test:
- 您使用
functions.config() - 您的函式會與 Firebase 專案或其他 Google API 互動,而您想在測試中使用實際的 Firebase 專案及其憑證。
HTTP onRequest 函式會採用兩個參數:要求物件和回應物件。以下是測試 addMessage() 範例函式的方法:
虛設常式
admin.database(),因為addMessage()會推送至即時資料庫。在離線模式下,您虛設了initializeApp(),因此 Admin SDK 沒有可用的 Firebase 應用程式,而實際的admin.database()會失敗,並顯示The default Firebase app does not exist:const pushStub = jest.fn().mockResolvedValue({ ref: 'new_ref' }); const refStub = jest.fn().mockReturnValue({ push: pushStub }); Object.defineProperty(admin, 'database', { configurable: true, writable: true, value: jest.fn().mockReturnValue({ ref: refStub }), });覆寫回應物件中的重新導向函式,因為
addMessage()會呼叫該函式。在重新導向函式中,使用 Jest 的
expect斷言應使用哪些參數呼叫重新導向函式:
// 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) => { expect(code).toBe(303); expect(url).toBe('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 初始化時設定的環境變數,並刪除可能已建立的 Firebase 應用程式 (如果您使用 SDK 建立即時資料庫 DataSnapshot 或 Firestore DocumentSnapshot)。
test.cleanup();
查看完整範例並瞭解詳情
您可以在 Firebase GitHub 存放區查看完整範例。
詳情請參閱 firebase-functions-test 的 API 參考資料。