1. 始める前に
Cloud Firestore や Cloud Functions のようなサーバーレス バックエンド ツールは使い方は簡単ですが、テストが難しい場合があります。Firebase Local Emulator Suite を使用すると、これらのサービスのローカル バージョンを開発マシンで実行できるため、アプリを迅速かつ安全に開発できます。
前提とする環境
- Visual Studio Code、Atom、Sublime Text などのシンプルなエディタ
- Node.js 10.0.0 以降(Node.js をインストールするには、nvm を使用してバージョンを確認し、
node --version
を実行します) - Java 7 以降(Java をインストールするには、こちらの手順に沿ってバージョンを確認し、
java -version
を実行します)
演習内容
この Codelab では、複数の Firebase サービスを利用したシンプルなオンライン ショッピング アプリを実行し、デバッグします。
- Cloud Firestore: グローバルにスケーラブルでサーバーレスの、リアルタイム機能を備えた NoSQL データベース。
- Cloud Functions: イベントまたは HTTP リクエストに応答して実行されるサーバーレス バックエンド コード。
- Firebase Authentication: 他の Firebase プロダクトと統合されるマネージド認証サービス。
- Firebase Hosting: ウェブアプリ用の高速で安全なホスティング。
アプリを Emulator Suite に接続して、ローカルでの開発を可能にします。
また、次の方法も説明します。
- アプリを Emulator Suite に接続する方法と、さまざまなエミュレータを接続する方法。
- Firebase セキュリティ ルールの仕組みと、Firestore セキュリティ ルールをローカル エミュレータに対してテストする方法について説明します。
- Firestore イベントによってトリガーされる Firebase 関数を作成する方法と、Emulator Suite に対して実行する統合テストを作成する方法。
2. 設定
ソースコードを取得する
この Codelab では、ほぼ完成したバージョンの The Fire Store サンプルから開始します。まず、ソースコードのクローンを作成する必要があります。
$ git clone https://github.com/firebase/emulators-codelab.git
次に、この Codelab ディレクトリに移動します。ここで、この Codelab の残りの部分を行います。
$ cd emulators-codelab/codelab-initial-state
次に、依存関係をインストールして、コードを実行できるようにします。インターネットの接続速度が遅い場合は、1 ~ 2 分かかることがあります。
# Move into the functions directory
$ cd functions
# Install dependencies
$ npm install
# Move back into the previous directory
$ cd ../
Firebase CLI を取得する
Emulator Suite は Firebase CLI(コマンドライン インターフェース)の一部であり、次のコマンドを使用してマシンにインストールできます。
$ npm install -g firebase-tools
次に、CLI が最新バージョンであることを確認します。この Codelab はバージョン 9.0.0 以降で動作しますが、以降のバージョンにはより多くのバグ修正が含まれています。
$ firebase --version 9.6.0
Firebase プロジェクトに接続する
Firebase プロジェクトがない場合は、Firebase コンソールで新しい Firebase プロジェクトを作成します。後で必要になるため、選択したプロジェクト ID をメモしておきます。
次に、このコードを Firebase プロジェクトに接続する必要があります。まず、次のコマンドを実行して Firebase CLI にログインします。
$ firebase login
次に、次のコマンドを実行してプロジェクト エイリアスを作成します。$YOUR_PROJECT_ID
は、Firebase プロジェクトの ID に置き換えます。
$ firebase use $YOUR_PROJECT_ID
これで、アプリを実行する準備が整いました。
3. エミュレータを実行する
このセクションでは、アプリをローカルで実行します。いよいよ Emulator Suite を起動します。
エミュレータを起動する
Codelab のソース ディレクトリ内から、次のコマンドを実行してエミュレータを起動します。
$ firebase emulators:start --import=./seed
次のような出力が表示されます。
$ firebase emulators:start --import=./seed i emulators: Starting emulators: auth, functions, firestore, hosting ⚠ functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: database, pubsub i firestore: Importing data from /Users/samstern/Projects/emulators-codelab/codelab-initial-state/seed/firestore_export/firestore_export.overall_export_metadata i firestore: Firestore Emulator logging to firestore-debug.log i hosting: Serving hosting files from: public ✔ hosting: Local server: http://127.0.0.1:5000 i ui: Emulator UI logging to ui-debug.log i functions: Watching "/Users/samstern/Projects/emulators-codelab/codelab-initial-state/functions" for Cloud Functions... ✔ functions[calculateCart]: firestore function initialized. ┌─────────────────────────────────────────────────────────────┐ │ ✔ All emulators ready! It is now safe to connect your app. │ │ i View Emulator UI at http://127.0.0.1:4000 │ └─────────────────────────────────────────────────────────────┘ ┌────────────────┬────────────────┬─────────────────────────────────┐ │ Emulator │ Host:Port │ View in Emulator UI │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Functions │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Firestore │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Hosting │ 127.0.0.1:5000 │ n/a │ └────────────────┴────────────────┴─────────────────────────────────┘ Emulator Hub running at 127.0.0.1:4400 Other reserved ports: 4500 Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.
「All emulators started」というメッセージが表示されたら、アプリは使用できる状態です。
ウェブアプリをエミュレータに接続する
ログの表を見ると、Cloud Firestore エミュレータがポート 8080
でリッスンし、Authentication エミュレータがポート 9099
でリッスンしていることがわかります。
┌────────────────┬────────────────┬─────────────────────────────────┐ │ Emulator │ Host:Port │ View in Emulator UI │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Functions │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Firestore │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Hosting │ 127.0.0.1:5000 │ n/a │ └────────────────┴────────────────┴─────────────────────────────────┘
フロントエンド コードを本番環境ではなくエミュレータに接続しましょう。public/js/homepage.js
ファイルを開き、onDocumentReady
関数を見つけます。このコードでは、標準の Firestore インスタンスと Auth インスタンスにアクセスしていることがわかります。
public/js/homepage.js
const auth = firebaseApp.auth();
const db = firebaseApp.firestore();
ローカル エミュレータを指すように db
オブジェクトと auth
オブジェクトを更新しましょう。
public/js/homepage.js
const auth = firebaseApp.auth();
const db = firebaseApp.firestore();
// ADD THESE LINES
if (location.hostname === "127.0.0.1") {
console.log("127.0.0.1 detected!");
auth.useEmulator("http://127.0.0.1:9099");
db.useEmulator("127.0.0.1", 8080);
}
アプリがローカルマシン(Hosting エミュレータによって提供されるもの)で実行されている場合、Firestore クライアントは本番環境のデータベースではなくローカル エミュレータも参照するようになりました。
EmulatorUI を開く
ウェブブラウザで http://127.0.0.1:4000/ に移動します。Emulator Suite UI が表示されます。
クリックすると、Firestore エミュレータの UI が表示されます。--import
フラグを使用してインポートしたデータであるため、items
コレクションにはすでにデータが含まれています。
4. アプリを実行する
アプリを開く
ウェブブラウザで http://127.0.0.1:5000 に移動すると、ローカルのマシン上で Fire Store が実行されていることがわかります。
アプリを使用する
ホームページで商品を選択し、[カートに追加] をクリックします。残念ながら、次のエラーが発生します。
このバグを修正しましょう。すべてがエミュレータで実行されているため、テストが可能で、実際のデータへの影響を心配する必要はありません。
5. アプリをデバッグする
バグを探す
Chrome のデベロッパーコンソールで見てみましょうControl+Shift+J
(Windows、Linux、ChromeOS)または Command+Option+J
(Mac)を押して、コンソールにエラーを表示します。
addToCart
メソッドでエラーが発生したようです。では、見てみましょう。このメソッドで uid
と呼ばれるものにアクセスするにはどうすればよいでしょうか。また、それが null
になるのはなぜでしょうか。現時点で、メソッドは public/js/homepage.js
で次のように記述されています。
public/js/homepage.js
addToCart(id, itemData) {
console.log("addToCart", id, JSON.stringify(itemData));
return this.db
.collection("carts")
.doc(this.auth.currentUser.uid)
.collection("items")
.doc(id)
.set(itemData);
}
ああ!アプリにログインしていません。Firebase Authentication のドキュメントによると、ログインしていない場合、auth.currentUser
は null
です。これを確認してみましょう。
public/js/homepage.js
addToCart(id, itemData) {
// ADD THESE LINES
if (this.auth.currentUser === null) {
this.showError("You must be signed in!");
return;
}
// ...
}
アプリをテストする
ページを更新して [カートに追加] をクリックします。今回は、より適切なエラーが表示されます。
しかし、上部のツールバーで [ログイン] をクリックしてからもう一度 [カートに追加] をクリックすると、カートが更新されます。
しかし、数字がまったく正確ではないようです。
このバグはすぐに修正される予定ですのでご安心ください。まず、カートに商品を追加したときに実際に何が起きたのかを詳しく見ていきましょう。
6. ローカル関数トリガー
[カートに追加] をクリックすると、複数のエミュレータを含む一連のイベントが開始されます。カートに商品を追加すると、Firebase CLI ログに次のようなメッセージが表示されるはずです。
i functions: Beginning execution of "calculateCart" i functions: Finished "calculateCart" in ~1s
これらのログの生成と UI の更新には、次の 4 つのキーイベントが発生しました。
1)Firestore の書き込み - クライアント
Firestore コレクション /carts/{cartId}/items/{itemId}/
に新しいドキュメントが追加されました。このコードは、public/js/homepage.js
内の addToCart
関数で確認できます。
public/js/homepage.js
addToCart(id, itemData) {
// ...
console.log("addToCart", id, JSON.stringify(itemData));
return this.db
.collection("carts")
.doc(this.auth.currentUser.uid)
.collection("items")
.doc(id)
.set(itemData);
}
2)Cloud Functions の関数をトリガー
Cloud Functions の関数 calculateCart
は、onWrite
トリガーを使用してカートのアイテムで発生する書き込みイベント(作成、更新、削除)をリッスンします。これは functions/index.js
で確認できます。
functions/index.js
exports.calculateCart = functions.firestore
.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
try {
let totalPrice = 125.98;
let itemCount = 8;
const cartRef = db.collection("carts").doc(context.params.cartId);
await cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
}
);
3)Firestore の書き込み - 管理者
calculateCart
関数は、カート内のすべての商品を読み取り、合計数量と価格を合計してから、「cart」ドキュメントを新しい合計で更新します(上記の cartRef.update(...)
を参照)。
4)Firestore 読み取り - クライアント
ウェブ フロントエンドは、カートの変更に関する最新情報を受け取るよう登録されています。public/js/homepage.js
で確認できるように、Cloud Functions の関数が新しい合計を書き込んで UI を更新した後、リアルタイムで更新されます。
public/js/homepage.js
this.cartUnsub = cartRef.onSnapshot(cart => {
// The cart document was changed, update the UI
// ...
});
内容のまとめ
お疲れさまでした。3 つの異なる Firebase エミュレータを使用して完全なローカルテストを行う、完全にローカルのアプリをセットアップできました。
他にもあります。次のセクションでは、次のことを説明します。
- Firebase エミュレータを使用する単体テストを作成する方法。
- Firebase エミュレータを使用してセキュリティ ルールをデバッグする方法。
7. アプリに合わせたセキュリティ ルールを作成する
ウェブアプリはデータの読み取りと書き込みを行いますが、今のところセキュリティについてはまったく心配していません。Cloud Firestore は「セキュリティ ルール」というシステムを使用して、データの読み取りと書き込みにアクセスできるユーザーを宣言します。Emulator Suite は、このようなルールのプロトタイプを作成するのに最適です。
エディタで emulators-codelab/codelab-initial-state/firestore.rules
ファイルを開きます。ルールには主に 3 つのセクションがあります。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// User's cart metadata
match /carts/{cartID} {
// TODO: Change these! Anyone can read or write.
allow read, write: if true;
}
// Items inside the user's cart
match /carts/{cartID}/items/{itemID} {
// TODO: Change these! Anyone can read or write.
allow read, write: if true;
}
// All items available in the store. Users can read
// items but never write them.
match /items/{itemID} {
allow read: if true;
}
}
}
現在、誰でもデータベースに対してデータの読み取りと書き込みを行うことができます。Google では、有効な処理のみを実行し、機密情報が漏洩しないようにしたいと考えています。
この Codelab では、最小権限の原則に沿って、すべてのドキュメントをロックダウンし、すべてのユーザーが必要なすべてのアクセス権を手に入れるまで少しずつアクセス権を追加しますが、それ以上のアクセス権は付与しません。条件を false
に設定して、アクセスを拒否するように最初の 2 つのルールを更新しましょう。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// User's cart metadata
match /carts/{cartID} {
// UPDATE THIS LINE
allow read, write: if false;
}
// Items inside the user's cart
match /carts/{cartID}/items/{itemID} {
// UPDATE THIS LINE
allow read, write: if false;
}
// All items available in the store. Users can read
// items but never write them.
match /items/{itemID} {
allow read: if true;
}
}
}
8. エミュレータとテストを実行する
エミュレータを起動する
コマンドラインで、emulators-codelab/codelab-initial-state/
にいることを確認します。ここまでの手順で作成したエミュレータが残っている場合があります。起動しない場合は、エミュレータを再起動します。
$ firebase emulators:start --import=./seed
エミュレータを実行したら、そのエミュレータに対してローカルでテストを実行できます。
テストを実行する
コマンドライン(新しいターミナルタブで、ディレクトリ emulators-codelab/codelab-initial-state/
から)
まず、関数ディレクトリに移動します(Codelab の残りの部分はここで使用します)。
$ cd functions
次に、関数ディレクトリで mocha テストを実行し、出力の先頭までスクロールします。
# Run the tests $ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping carts 1) can be created and updated by the cart owner 2) can be read only by the cart owner shopping cart items 3) can be read only by the cart owner 4) can be added only by the cart owner adding an item to the cart recalculates the cart total. - should sum the cost of their items 0 passing (364ms) 1 pending 4 failing
今のところ、4 つの失敗があります。ルールファイルを作成しながら、より多くのテストに合格するのを確認することで、進行状況を測定できます。
9. カートへの安全なアクセス
最初の 2 つの失敗は、以下をテストする「ショッピング カート」テストです。
- ユーザーは自分のカートのみを作成、更新できます
- ユーザーは自分のカートのみを読み取ることができます
functions/test.js
it('can be created and updated by the cart owner', async () => {
// Alice can create her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").set({
ownerUID: "alice",
total: 0
}));
// Bob can't create Alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart").set({
ownerUID: "alice",
total: 0
}));
// Alice can update her own cart with a new total
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").update({
total: 1
}));
// Bob can't update Alice's cart with a new total
await firebase.assertFails(bobDb.doc("carts/alicesCart").update({
total: 1
}));
});
it("can be read only by the cart owner", async () => {
// Setup: Create Alice's cart as admin
await admin.doc("carts/alicesCart").set({
ownerUID: "alice",
total: 0
});
// Alice can read her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").get());
// Bob can't read Alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart").get());
});
これらのテストに合格しましょう。エディタでセキュリティ ルール ファイル firestore.rules
を開き、match /carts/{cartID}
内のステートメントを更新します。
firestore.rules
rules_version = '2';
service cloud.firestore {
// UPDATE THESE LINES
match /carts/{cartID} {
allow create: if request.auth.uid == request.resource.data.ownerUID;
allow read, update, delete: if request.auth.uid == resource.data.ownerUID;
}
// ...
}
}
これらのルールにより、カート所有者による読み取りと書き込みのアクセスのみが許可されるようになりました。
受信データとユーザーの認証を検証するために、すべてのルールのコンテキストで使用可能な次の 2 つのオブジェクトを使用します。
request
オブジェクトには、試行されているオペレーションに関するデータとメタデータが格納されます。- Firebase プロジェクトで Firebase Authentication を使用している場合、
request.auth
オブジェクトにはリクエストを行っているユーザーが記述されます。
10. カートへのアクセスをテストする
firestore.rules
が保存されるたびに、Emulator Suite はルールを自動的に更新します。エミュレータを実行しているタブでメッセージ Rules updated
を確認することで、エミュレータでルールが更新されたことを確認できます。
テストを再実行し、最初の 2 つのテストに合格することを確認します。
$ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping carts ✓ can be created and updated by the cart owner (195ms) ✓ can be read only by the cart owner (136ms) shopping cart items 1) can be read only by the cart owner 2) can be added only by the cart owner adding an item to the cart recalculates the cart total. - should sum the cost of their items 2 passing (482ms) 1 pending 2 failing
よくできました!これでショッピング カートに確実にアクセスできるようになりました。次の失敗するテストに進みます。
11. UI で「カートに追加」フローを確認します。
現時点では、カートのオーナーはカートの読み取りと書き込みを行えますが、カート内の個々の商品の読み取りと書き込みを行うことはできません。オーナーはカート ドキュメントにアクセスできますが、カートの items サブコレクションにはアクセスできないためです。
これはユーザーにとって無効な状態です。
http://127.0.0.1:5000,
で実行されているウェブ UI に戻り、カートに商品を追加してみます。items
サブコレクションで作成されたドキュメントへのアクセス権がまだユーザーに付与されていないため、デバッグ コンソールに Permission Denied
エラーが表示されます。
12. カート アイテムへのアクセスを許可する
次の 2 つのテストにより、ユーザーが自分のカートへの商品の追加とカートからの読み取りのみを行えることを確認します。
it("can be read only by the cart owner", async () => {
// Alice can read items in her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart/items/milk").get());
// Bob can't read items in alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart/items/milk").get())
});
it("can be added only by the cart owner", async () => {
// Alice can add an item to her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart/items/lemon").set({
name: "lemon",
price: 0.99
}));
// Bob can't add an item to alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart/items/lemon").set({
name: "lemon",
price: 0.99
}));
});
そのため、現在のユーザーの UID がカート ドキュメントの ownerUID と同じ場合にアクセスを許可するルールを作成できます。create, update, delete
に別のルールを指定する必要はないため、write
ルールを使用できます。これは、データを変更するすべてのリクエストに適用されます。
items サブコレクション内のドキュメントのルールを更新します。条件の get
は、Firestore(この場合はカート ドキュメントの ownerUID
)から値を読み取っています。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ...
// UPDATE THESE LINES
match /carts/{cartID}/items/{itemID} {
allow read, write: if get(/databases/$(database)/documents/carts/$(cartID)).data.ownerUID == request.auth.uid;
}
// ...
}
}
13. カート アイテムへのアクセスをテストする
これで、テストを再実行できます。出力の先頭までスクロールして、他のテストにも合格していることを確認します。
$ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping carts ✓ can be created and updated by the cart owner (195ms) ✓ can be read only by the cart owner (136ms) shopping cart items ✓ can be read only by the cart owner (111ms) ✓ can be added only by the cart owner adding an item to the cart recalculates the cart total. - should sum the cost of their items 4 passing (401ms) 1 pending
問題なし。これで、すべてのテストに合格しました。保留中のテストが 1 つありますが、数ステップで実行できます。
14. 「カートに追加」フローをもう一度確認します。
ウェブ フロントエンド(http://127.0.0.1:5000)に戻り、カートに商品を追加します。これは、テストとルールがクライアントが必要とする機能と一致していることを確認する重要なステップです。(前回 UI を試したとき、カートに商品を追加できなかったことを思い出してください)。
firestore.rules
が保存されると、クライアントはルールを自動的に再読み込みします。では、カートに商品を追加してみましょう。
内容のまとめ
これで、これで、アプリのセキュリティが強化されました。これは、本番環境に備えるために不可欠なステップです。これが本番環境のアプリであれば、継続的インテグレーション パイプラインにこれらのテストを追加できます。これにより、今後、他のユーザーがルールを変更しても、ショッピング カートのデータにこれらのアクセス制御が適用されることを確信できます。
まだまだ続きます
学習を続けると、次のことがわかります。
- Firestore イベントによってトリガーされる関数を記述する方法
- 複数のエミュレータで動作するテストの作成方法
15. Cloud Functions テストを設定する
ここまでは、ウェブアプリのフロントエンドと Firestore セキュリティ ルールに焦点を当ててきました。しかし、このアプリは Cloud Functions を使用してユーザーのカートを最新の状態に保つため、そのコードもテストします。
Emulator Suite を使用すると、Cloud Firestore やその他のサービスを使用する関数だけでなく、Cloud Functions も簡単にテストできます。
エディタで emulators-codelab/codelab-initial-state/functions/test.js
ファイルを開き、ファイル内の最後のテストまでスクロールします。現在は保留中とマークされています。
// REMOVE .skip FROM THIS LINE
describe.skip("adding an item to the cart recalculates the cart total. ", () => {
// ...
it("should sum the cost of their items", async () => {
...
});
});
テストを有効にするには、次のように .skip
を削除します。
describe("adding an item to the cart recalculates the cart total. ", () => {
// ...
it("should sum the cost of their items", async () => {
...
});
});
次に、ファイルの先頭で REAL_FIREBASE_PROJECT_ID
変数を見つけて、実際の Firebase プロジェクト ID に変更します。
// CHANGE THIS LINE
const REAL_FIREBASE_PROJECT_ID = "changeme";
プロジェクト ID を忘れた場合は、Firebase コンソールの [プロジェクトの設定] で Firebase プロジェクト ID を確認できます。
16. Functions のテストのチュートリアル
このテストでは Cloud Firestore と Cloud Functions のインタラクションを検証するため、前の Codelab のテストよりも多くのセットアップが必要になります。このテストをひととおり確認し、このテストで想定される内容を把握しましょう。
カートを作成する
Cloud Functions は信頼できるサーバー環境で実行され、Admin SDK で使用されるサービス アカウント認証を使用できます。まず、initializeApp
ではなく initializeAdminApp
を使用してアプリを初期化します。次に、商品を追加するカートの DocumentReference を作成し、カートを初期化します。
it("should sum the cost of their items", async () => {
const db = firebase
.initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
.firestore();
// Setup: Initialize cart
const aliceCartRef = db.doc("carts/alice")
await aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });
...
});
関数をトリガーする
次に、カート ドキュメントの items
サブコレクションにドキュメントを追加して、関数をトリガーします。関数内で行われる追加をテストできるように、アイテムを 2 つ追加します。
it("should sum the cost of their items", async () => {
const db = firebase
.initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
.firestore();
// Setup: Initialize cart
const aliceCartRef = db.doc("carts/alice")
await aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });
// Trigger calculateCart by adding items to the cart
const aliceItemsRef = aliceCartRef.collection("items");
await aliceItemsRef.doc("doc1").set({name: "nectarine", price: 2.99});
await aliceItemsRef.doc("doc2").set({ name: "grapefruit", price: 6.99 });
...
});
});
テストの流れを伝える
onSnapshot()
を使用して、カート ドキュメントの変更に対するリスナーを登録します。onSnapshot()
は、リスナーの登録を解除するために呼び出すことができる関数を返します。
このテストでは、合計 9.98 ドルのアイテムを 2 つ追加します。次に、カートに想定どおりの itemCount
と totalPrice
があるかどうかを確認します。もしそうなら、関数がその役目を果たしたということです。
it("should sum the cost of their items", (done) => {
const db = firebase
.initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
.firestore();
// Setup: Initialize cart
const aliceCartRef = db.doc("carts/alice")
aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });
// Trigger calculateCart by adding items to the cart
const aliceItemsRef = aliceCartRef.collection("items");
aliceItemsRef.doc("doc1").set({name: "nectarine", price: 2.99});
aliceItemsRef.doc("doc2").set({ name: "grapefruit", price: 6.99 });
// Listen for every update to the cart. Every time an item is added to
// the cart's subcollection of items, the function updates `totalPrice`
// and `itemCount` attributes on the cart.
// Returns a function that can be called to unsubscribe the listener.
await new Promise((resolve) => {
const unsubscribe = aliceCartRef.onSnapshot(snap => {
// If the function worked, these will be cart's final attributes.
const expectedCount = 2;
const expectedTotal = 9.98;
// When the `itemCount`and `totalPrice` match the expectations for the
// two items added, the promise resolves, and the test passes.
if (snap.data().itemCount === expectedCount && snap.data().totalPrice == expectedTotal) {
// Call the function returned by `onSnapshot` to unsubscribe from updates
unsubscribe();
resolve();
};
});
});
});
});
17. テストを実行する
以前のテストで使用したエミュレータが残っている場合があります。起動しない場合は、エミュレータを起動します。コマンドラインから、次のコマンドを実行します。
$ firebase emulators:start --import=./seed
新しいターミナルタブを開き(エミュレータを実行したまま)、関数ディレクトリに移動します。セキュリティ ルールのテストで、まだこの状態のままになっている可能性があります。
$ cd functions
単体テストを実行すると、合計 5 つのテストが表示されます。
$ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping cart creation ✓ can be created by the cart owner (82ms) shopping cart reads, updates, and deletes ✓ cart can be read by the cart owner (42ms) shopping cart items ✓ items can be read by the cart owner (40ms) ✓ items can be added by the cart owner adding an item to the cart recalculates the cart total. 1) should sum the cost of their items 4 passing (2s) 1 failing
具体的なエラーを見ると、タイムアウト エラーのように見えます。これは、テストは関数が正しく更新されるのを待機しているが、更新されないからです。これで、テストを満たす関数を作成する準備が整いました。
18. 関数を記述する
このテストを修正するには、functions/index.js
の関数を更新する必要があります。この関数の一部は記述されていますが、完成していません。現在の関数は次のようになります。
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
let totalPrice = 125.98;
let itemCount = 8;
try {
const cartRef = db.collection("carts").doc(context.params.cartId);
await cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
});
この関数はカート参照を正しく設定していますが、totalPrice
と itemCount
の値を計算する代わりに、ハードコードされた値に更新します。
API をフェッチして反復処理し、
items
サブコレクション
items
サブコレクションとなる新しい定数 itemsSnap
を初期化します。次に、コレクション内のすべてのドキュメントを反復処理します。
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
try {
let totalPrice = 125.98;
let itemCount = 8;
const cartRef = db.collection("carts").doc(context.params.cartId);
// ADD LINES FROM HERE
const itemsSnap = await cartRef.collection("items").get();
itemsSnap.docs.forEach(item => {
const itemData = item.data();
})
// TO HERE
return cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
});
totalPrice と itemCount を計算する
まず、totalPrice
と itemCount
の値を 0 に初期化します。
次に、反復ブロックにロジックを追加します。まず、アイテムに価格が設定されていることを確認します。商品アイテムの数量が指定されていない場合は、デフォルトの 1
を指定します。次に、この数量を itemCount
の累積合計に追加します。最後に、商品アイテムの価格に数量を掛けた値を合計 totalPrice
に加算します。
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
try {
// CHANGE THESE LINES
let totalPrice = 0;
let itemCount = 0;
const cartRef = db.collection("carts").doc(context.params.cartId);
const itemsSnap = await cartRef.collection("items").get();
itemsSnap.docs.forEach(item => {
const itemData = item.data();
// ADD LINES FROM HERE
if (itemData.price) {
// If not specified, the quantity is 1
const quantity = itemData.quantity ? itemData.quantity : 1;
itemCount += quantity;
totalPrice += (itemData.price * quantity);
}
// TO HERE
})
await cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
});
ロギングを追加して、成功とエラーの状態をデバッグすることもできます。
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
let totalPrice = 0;
let itemCount = 0;
try {
const cartRef = db.collection("carts").doc(context.params.cartId);
const itemsSnap = await cartRef.collection("items").get();
itemsSnap.docs.forEach(item => {
const itemData = item.data();
if (itemData.price) {
// If not specified, the quantity is 1
const quantity = (itemData.quantity) ? itemData.quantity : 1;
itemCount += quantity;
totalPrice += (itemData.price * quantity);
}
});
await cartRef.update({
totalPrice,
itemCount
});
// OPTIONAL LOGGING HERE
console.log("Cart total successfully recalculated: ", totalPrice);
} catch(err) {
// OPTIONAL LOGGING HERE
console.warn("update error", err);
}
});
19. テストを再実行する
コマンドラインで、エミュレータがまだ動作していることを確認し、テストを再実行します。エミュレータは関数の変更を自動的に取得するため、エミュレータを再起動する必要はありません。すべてのテストに合格したことがわかります。
$ npm test > functions@ test .../emulators-codelab/codelab-initial-state/functions > mocha shopping cart creation ✓ can be created by the cart owner (306ms) shopping cart reads, updates, and deletes ✓ cart can be read by the cart owner (59ms) shopping cart items ✓ items can be read by the cart owner ✓ items can be added by the cart owner adding an item to the cart recalculates the cart total. ✓ should sum the cost of their items (800ms) 5 passing (1s)
よくできました!
20. ストアフロント UI を使用して試す
最後のテストでは、ウェブアプリ(http://127.0.0.1:5000/)に戻り、アイテムをカートに追加します。
カートが更新され、正しい合計数が表示されることを確認します。ありがとうございます。
内容のまとめ
Cloud Functions for Firebase と Cloud Firestore の複雑なテストケースについて確認しました。テストに合格するために Cloud Functions の関数を記述しました。また、UI で新機能が動作していることも確認できました。すべてローカルで実行し、エミュレータを自分のマシンで実行しました。
また、ローカル エミュレータで実行するウェブ クライアントを作成し、データを保護するためのセキュリティ ルールをカスタマイズして、ローカル エミュレータを使用してセキュリティ ルールをテストしました。