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-test とテスト フレームワークの Mocha の両方をインストールします。

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 を使用するには、次の 2 つの方法があります。

  1. オフライン モード: 副作用なしにサイロ化したオフラインの単体テストを作成します。つまり、Firebase プロダクトとやり取りするメソッド呼び出し(たとえば、データベースへの書き込みやユーザーの作成)はすべてスタブされる必要があります。
  2. オンライン モード: データベースへの書き込みやユーザーの作成などが実際に行われ、テストコードがその結果を検査できるように、テスト専用の Firebase プロジェクトとやり取りするテストを作成します。また、関数で使用される他の Google SDK が機能することも意味します。

オフライン モードで SDK を初期化する

完全にオフラインのテストを作成する場合は、パラメータを指定せずに SDK を初期化できます。

// At the top of test/index.test.js
const test = require('firebase-functions-test')();

オンライン モードで SDK を初期化する

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

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

  1. Firebase コンソールに移動します。
  2. プロジェクトを選択し、[ウェブアプリに Firebase を追加] をクリックすると、構成値がポップアップに表示されます。

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

  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');

構成値のモック

関数コードで 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.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 番目のパラメータです。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 つのスナップショットを作成する必要があります。次に、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 または Database 関数をテストしている場合、実際の DocumentSnapshot または DataSnapshot を作成してメソッドをスタブするよりも、スタブとともにプレーン オブジェクトを使用するほうが簡単だと思われることがあります。

次の関数の単体テストを作成しているとします。

exports.makeUppercase = functions.database.ref('/messages/{pushId}/original')
    .onCreate((snap, context) => {
// [END makeUppercaseTrigger]
      // [START makeUppercaseBody]
      // Grab the current value of what was written to the Realtime Database.
      const original = snap.val();
      console.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 snap.ref.parent.child('uppercase').set(uppercase);
      // [END makeUppercaseBody]
    });


関数の内部で 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 などのライブラリを使用できます。

オフライン モードでのアサーションの作成

関数の予期される戻り値に関するアサーションを作成できます。

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 スパイを使用することにより、想定されたパラメータを指定して特定のメソッドが呼び出されたことに関するアサートを行うことができます。

オンライン モードでのアサーションの作成

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');
  });
});

HTTP 関数のテスト

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 リファレンスをご覧ください。

フィードバックを送信...

ご不明な点がありましたら、Google のサポートページをご覧ください。