Firebase Summit で発表されたすべての情報をご覧ください。Firebase を使用してアプリ開発を加速し、自信を持ってアプリを実行する方法を紹介しています。詳細

クラウド関数の単体テスト

コレクションでコンテンツを整理 必要に応じて、コンテンツの保存と分類を行います。

このページでは、継続的インテグレーション (CI) システムの一部となるテストなど、関数の単体テストを作成するためのベスト プラクティスとツールについて説明します。テストを容易にするために、Firebase には Cloud Functions 用の Firebase Test SDK が用意されています。これは npm でfirebase-functions-testとして配布されており、firebase- firebase-functionsのコンパニオン テスト SDK です。 Cloud Functions 用の Firebase Test SDK:

  • firebase firebase-functions必要な環境変数の設定と設定解除など、テストの適切なセットアップとティアダウンを処理します。
  • テストに関連するフィールドのみを指定する必要があるように、サンプル データとイベント コンテキストを生成します。

テスト設定

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 Test SDK の初期化

firebase-functions-testを使用するには 2 つの方法があります。

  1. オンライン モード (推奨):テスト専用の Firebase プロジェクトと対話するテストを作成して、データベースの書き込み、ユーザーの作成などが実際に行われ、テスト コードが結果を検査できるようにします。これは、関数で使用されている他の Google SDK も同様に機能することを意味します。
  2. オフライン モード:サイロ化されたオフラインの単体テストを副作用なしで記述します。これは、Firebase 製品とやり取りするメソッド呼び出し (データベースへの書き込みやユーザーの作成など) をスタブ化する必要があることを意味します。テスト コードが非常に複雑になるため、Cloud Firestore または Realtime Database 機能がある場合、オフライン モードの使用は一般に推奨されません。

SDK をオンライン モードで初期化する (推奨)

テスト プロジェクトとやり取りするテストを作成する場合は、 firebase-adminを使用してアプリを初期化するために必要なプロジェクト構成値と、サービス アカウント キー ファイルへのパスを指定する必要があります。

Firebase プロジェクトの構成値を取得するには:

  1. Firebase コンソールでプロジェクト設定を開きます。
  2. [あなたのアプリ] で、目的のアプリを選択します。
  3. 右側のペインで、Apple および Android アプリの構成ファイルをダウンロードするオプションを選択します。

    Web アプリの場合は、[構成]を選択して構成値を表示します。

鍵ファイルを作成するには:

  1. Google Cloud Console の [サービス アカウント] ペインを開きます。
  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 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.jsmakeUppercaseという関数があり、これをテストしたいとします。 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 (オプション): 指定するイベント コンテキストのフィールド。イベント コンテキストは、作成した関数ハンドラーに送信される 2 番目のパラメーターです。 wrap 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を作成します。最初の引数はデータで、2 番目の引数は完全な参照パスです。指定できるスナップショットの他のプロパティのオプションの 3 番目の引数があります。

// 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関数をテストする場合は、2 つのスナップショットを作成する必要があります。1 つは前の状態用、もう 1 つは後の状態用です。次に、 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が 2 回使用されます。

  • 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 関数は、要求オブジェクトと応答オブジェクトの 2 つのパラメーターを取ります。 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 リファレンスを参照してください。