雲功能的單元測試

此頁面描述了為您的功能編寫單元測試的最佳實踐和工具,例如將成為持續集成 (CI) 系統一部分的測試。為了簡化測試,Firebase 為 Cloud Functions 提供了 Firebase 測試 SDK。它分佈在NPM作為firebase-functions-test ,是一個配套的測試SDK,以firebase-functions 。適用於 Cloud Functions 的 Firebase 測試 SDK:

  • 負責相應的設置,並拆除了你的測試,如需要設置和取消環境變量firebase-functions
  • 生成示例數據和事件上下文,因此您只需指定與測試相關的字段。

測試設置

安裝這兩個firebase-functions-test摩卡,一個測試框架,通過在功能運行以下命令文件夾:

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 測試 SDK

有兩種使用兩種方法firebase-functions-test

  1. 聯機模式(推薦):編寫測試是用火力地堡項目相互作用專用於測試,使數據庫寫入,用戶創建等會真正發生,你的測試代碼可以檢查結果。這也意味著您的函數中使用的其他 Google SDK 也可以使用。
  2. 離線模式:寫孤立和無副作用的離線單元測試。這意味著任何與 Firebase 產品交互的方法調用(例如寫入數據庫或創建用戶)都需要存根。如果您有 Cloud Firestore 或實時數據庫功能,通常不建議使用離線模式,因為它會大大增加測試代碼的複雜性。

在線模式初始化SDK(推薦)

如果你想編寫測試,以測試項目互動,您需要提供一個時,需要在初始化程序的項目配置值firebase-admin ,和路徑服務帳戶密鑰文件。

要獲取 Firebase 項目的配置值:

  1. 在打開的項目設置火力地堡控制台
  2. 你的應用程序選擇所需的應用程序。
  3. 在右側窗格中,選擇下載 Apple 和 Android 應用程序配置文件的選項。

    對於網絡應用程序,選擇配置以顯示的配置值。

要創建密鑰文件:

  1. 打開服務帳戶窗格中的谷歌雲控制台。
  2. 選擇App Engine的默認服務帳戶,並使用選項菜單在合適的選擇創建關鍵
  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需要兩個參數:

  1. 數據(必需):數據要發送到makeUppercase 。這直接對應於發送到您編寫的函數處理程序的第一個參數。 firebase-functions-test提供了一種構建自定義數據或例如數據的方法。
  2. eventContextOptions(可選):事件上下文的領域,你想指定。事件上下文是發送到您編寫的函數處理程序的第二個參數。如果不包括eventContextOptions時調用參數wrapped ,仍然與明智的場產生的事件上下文。您可以通過在此處指定來覆蓋某些生成的字段。請注意,您只需包含要覆蓋的字段。您未覆蓋的任何字段都會生成。
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,並測試雲公司的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)

在測試代碼,創建純對象其中兩個上述兩種代碼路徑的將工作,並使用興農存根方法。

// 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、封裝函數、構造數據後,您可以使用構造的數據調用封裝的函數並對行為進行斷言。您可以使用庫如做出這些斷言。

在在線模式下進行斷言

如果您在初始化的火力地堡測試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);

您也可以使用興農間諜斷言某些方法被調用,並與參數是否符合預期。

測試 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

test.cleanup();

查看完整示例並了解更多信息

您可以查看 Firebase GitHub 存儲庫上的完整示例。

要了解更多信息,請參閱API參考firebase-functions-test