このページでは、継続的インテグレーション(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 つの方法があります。
- オンライン モード(推奨): データベースへの書き込みやユーザーの作成などが実際に行われ、テストコードによってその結果を検査できるように、テスト専用の Firebase プロジェクトとやり取りするテストを作成します。この場合、関数で使用される他の Google SDK も機能します。
- オフライン モード: 副作用が発生しない、サイロ化したオフラインの単体テストを作成します。つまり、Firebase プロダクトとやり取りするメソッド呼び出し(たとえば、データベースへの書き込みやユーザーの作成)はすべてスタブされる必要があります。Cloud Firestore または Realtime Database の関数を使用している場合は、テストコードが非常に複雑になるため、オフライン モードの使用は通常おすすめしません。
オンライン モードで SDK を初期化する(推奨)
テスト プロジェクトとやり取りするテストを作成する場合は、firebase-admin
を通じてアプリを初期化するのに必要なプロジェクト構成値と、サービス アカウント キー ファイルへのパスを指定する必要があります。
Firebase プロジェクトの構成値を取得するには:
- Firebase コンソールでプロジェクト設定を開きます。
- [マイアプリ] で、目的のアプリを選択します。
右側のペインで、Apple アプリと Android アプリの構成ファイルをダウンロードするオプションを選択します。
ウェブアプリの場合は、[構成] を選択して構成値を表示します。
キーファイルを作成するには:
- Google Cloud コンソールの [サービス アカウント] ペインを開きます。
- App Engine のデフォルトのサービス アカウントを選択し、右側のオプション メニューを使用して [キーを作成] を選択します。
- プロンプトが表示されたら、キーのタイプとして 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 関数をテストするプロセスには、次の手順が含まれます。
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(省略可): 指定するイベント コンテキストのフィールド。イベント コンテキストは、作成した関数ハンドラに送信される 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 を初期化し、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-test
の API リファレンスをご覧ください。