Cloud Functions 的單元測試

本頁說明為函式編寫單元測試的最佳做法和工具,例如屬於持續整合 (CI) 系統的測試。為簡化測試作業,Firebase 提供 Cloud Functions 專用的 Firebase Test SDK。它是以 firebase-functions-test 的形式在 npm 上發布,也是 firebase-functions 的隨附測試 SDK。Cloud Functions 專用的 Firebase Test SDK:

  • 為測試進行適當的設定和拆除作業,例如設定和取消設定 firebase-functions 所需的環境變數。
  • 產生範例資料和事件結構定義,因此您只需要指定與測試相關的欄位。

測試設定

在函式資料夾中執行下列指令,以安裝 firebase-functions-testMocha:測試架構:

npm install --save-dev firebase-functions-test
npm install --save-dev mocha

接下來,請在函式資料夾內建立 test 資料夾,並在其中為測試程式碼建立新檔案,並以 index.test.js 這類名稱命名。

最後,修改 functions/package.json 來新增以下內容:

"scripts": {
  "test": "mocha --reporter spec"
}

撰寫測試後,您可以在函式目錄中執行 npm test 來執行測試。

初始化 Cloud Functions 適用的 Firebase Test SDK

使用 firebase-functions-test 的方式有兩種:

  1. 線上模式 (建議):撰寫與 Firebase 專案互動的測試,以便資料庫寫入、使用者建立等作業,而您的測試程式碼能夠檢查結果。這也表示函式中使用的其他 Google SDK 也可以正常運作。
  2. 離線模式:撰寫孤立和離線單元測試,但沒有任何副作用。 也就是說,任何與 Firebase 產品互動的方法呼叫 (例如寫入資料庫或建立使用者) 都必須以虛設常式呼叫。如果您有 Cloud Firestore 或即時資料庫函式,通常不建議使用離線模式,因為這會大幅增加測試程式碼的複雜度。

在線上模式中初始化 SDK (建議)

如要編寫與測試專案互動的測試,您必須透過 firebase-admin 提供初始化應用程式所需的專案設定值,以及服務帳戶金鑰檔案的路徑。

如要取得 Firebase 專案的設定值,請按照下列步驟操作:

  1. Firebase 控制台開啟專案設定。
  2. 在「你的應用程式」中,選取所需應用程式。
  3. 在右側窗格中,選取 Apple 和 Android 應用程式的下載設定檔選項。

    如果是網頁應用程式,選取「Config」即可顯示設定值。

如何建立金鑰檔案:

  1. 開啟 Google Cloud 控制台的「Service Accounts」(服務帳戶) 窗格
  2. 選取 App Engine 預設服務帳戶,然後在右側的選項選單中選擇「Create key」(建立金鑰)
  3. 系統提示時,請選取「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 函式的程序包含下列步驟:

  1. 使用 test.wrap 方法納入要測試的函式
  2. 建構測試資料
  3. 使用您建構的測試資料,以及想指定的任何事件內容欄位,叫用已包裝的函式。
  4. 針對行為做出斷言。

首先,納入您要測試的函式。假設您在 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 個參數:

  1. data (必要):要傳送至 makeUppercase 的資料。這會直接對應您編寫的函式處理常式的第一個參數。firebase-functions-test 提供建構自訂資料或範例資料的方法。
  2. 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);

如果您測試的是 onUpdateonWrite 函式,則需要建立兩個快照:一個用於之前狀態,另一個則用於之後狀態。接著,您可以使用 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 或即時資料庫函式,則應使用含虛設常式的純物件,而非建立實際的 DocumentSnapshotDataSnapshot

假設您要為下列函式編寫單元測試:

// 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 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. 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-testAPI 參考資料