Firebase EmulatorSuiteを使用したローカル開発

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

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 : Web アプリの高速で安全なホスティング。

アプリを Emulator Suite に接続して、ローカル開発を有効にします。

2589e2f95b74fa88.png

また、次の方法についても学習します。

  • アプリを Emulator Suite に接続する方法と、さまざまなエミュレータを接続する方法。
  • Firebase セキュリティ ルールの仕組みと、ローカル エミュレータに対して Firestore セキュリティ ルールをテストする方法。
  • Firestore イベントによってトリガーされる Firebase Function を作成する方法と、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://localhost: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://localhost:4000                │
└─────────────────────────────────────────────────────────────┘

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ localhost:5001 │ http://localhost:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ localhost:8080 │ http://localhost:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting        │ localhost:5000 │ n/a                             │
└────────────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at localhost: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メッセージが表示されたら、アプリを使用する準備ができています。

Web アプリをエミュレーターに接続する

ログの表に基づいて、Cloud Firestore エミュレーターがポート8080でリッスンし、Authentication エミュレーターがポート9099でリッスンしていることがわかります。

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ localhost:5001 │ http://localhost:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ localhost:8080 │ http://localhost:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting        │ localhost: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 === "localhost") {
    console.log("localhost detected!");
    auth.useEmulator("http://localhost:9099");
    db.useEmulator("localhost", 8080);
  }

アプリが localhost (ホスティング エミュレーターによって提供される) で実行されている場合、Firestore クライアントは、運用データベースではなく、ローカル エミュレーターもポイントします。

エミュレータUIを開く

Web ブラウザーで、 http://localhost:4000/に移動します。 Emulator Suite UI が表示されます。

エミュレーター UI のホーム画面

クリックして、Firestore エミュレーターの UI を表示します。 --importフラグを使用してデータをインポートしたため、 itemsコレクションには既にデータが含まれています。

4ef88d0148405d36.png

4. アプリを実行する

アプリを開く

Web ブラウザーでhttp://localhost:5000に移動すると、マシンでローカルに実行されている Fire Store が表示されます。

939f87946bac2ee4.png

アプリを使用する

ホームページでアイテムを選択し、[カートに追加] をクリックします。残念ながら、次のエラーが発生します。

a11bd59933a8e885.png

そのバグを修正しましょう!すべてがエミュレーターで実行されているため、実際のデータへの影響を心配せずに実験できます。

5. アプリをデバッグする

バグを見つける

では、Chrome 開発者コンソールを見てみましょう。 Control+Shift+J (Windows、Linux、Chrome OS) またはCommand+Option+J (Mac) を押して、コンソールにエラーを表示します。

74c45df55291dab1.png

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 docs によると、サインインしていない場合、 auth.currentUsernullです。そのためのチェックを追加しましょう:

public/js/homepage.js

  addToCart(id, itemData) {
    // ADD THESE LINES
    if (this.auth.currentUser === null) {
      this.showError("You must be signed in!");
      return;
    }

    // ...
  }

アプリをテストする

ここで、ページを更新して、[カートに追加] をクリックします。今回は、より適切なエラーが発生するはずです。

c65f6c05588133f7.png

ただし、上部のツールバーで [サインイン] をクリックしてから、[カートに追加] をもう一度クリックすると、カートが更新されていることがわかります。

ただし、数字がまったく正しいようには見えません。

239f26f02f959eef.png

心配しないで、すぐにそのバグを修正します。まず、商品をカートに追加したときに実際に何が起こったのかを詳しく見ていきましょう。

6. ローカル機能のトリガー

[カートに追加] をクリックすると、複数のエミュレーターが関与する一連のイベントが開始されます。カートにアイテムを追加すると、Firebase CLI ログに次のようなメッセージが表示されます。

i  functions: Beginning execution of "calculateCart"
i  functions: Finished "calculateCart" in ~1s

これらのログを生成するために発生した 4 つの重要なイベントと、観察した UI の更新がありました。

68c9323f2ad10f7a.png

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 Function calculateCartは、 functions/index.jsで確認できるonWriteトリガーを使用して、カート項目に発生するすべての書き込みイベント (作成、更新、または削除) をリッスンします。

関数/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関数は、カート内のすべてのアイテムを読み取り、合計数量と価格を合計してから、「カート」ドキュメントを新しい合計で更新します (上記のcartRef.update(...)を参照)。

4) Firestore 読み取り - クライアント

Web フロントエンドは、カートへの変更に関する更新を受信するためにサブスクライブされます。 public/js/homepage.jsで確認できるように、Cloud Function が新しい合計を書き込み、UI を更新した後、リアルタイムで更新されます。

public/js/homepage.js

this.cartUnsub = cartRef.onSnapshot(cart => {
   // The cart document was changed, update the UI
   // ...
});

要約

よくやった!完全にローカルなテスト用に 3 つの異なる Firebase エミュレーターを使用する完全にローカルなアプリをセットアップしました。

db82eef1706c9058.gif

しかし、待ってください。次のセクションでは、次のことを学びます。

  • Firebase Emulator を使用する単体テストを作成する方法。
  • Firebase Emulator を使用してセキュリティ ルールをデバッグする方法。

7. アプリに合わせたセキュリティ ルールを作成する

私たちの Web アプリはデータの読み取りと書き込みを行いますが、これまでのところ、セキュリティについてまったく心配していません。 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;
    }
  }
}

現在、誰でもデータベースのデータを読み書きできます。有効な操作のみが通過し、機密情報が漏洩しないようにしたいと考えています。

この 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/の新しいターミナル タブのコマンドライン

まず functions ディレクトリに移動します (コードラボの残りの部分はここにとどまります)。

$ cd functions

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 つの失敗は、次のことをテストする「ショッピング カート」テストです。

  • ユーザーは自分のカートのみを作成および更新できます
  • ユーザーは自分のカートのみを読み取ることができます

関数/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.カートへのアクセスをテストする

Emulator Suite は、 firestore.rulesが保存されるたびにルールを自動的に更新します。エミュレーターを実行しているタブで「ルールがRules updatedました」というメッセージを確認することで、エミュレーターがルールを更新したことを確認できます。

5680da418b420226.png

テストを再実行し、最初の 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://localhost:5000,実行されている Web 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
    }));
  });

そのため、現在のユーザーがカート ドキュメントの ownerUID と同じ UID を持っている場合にアクセスを許可するルールを作成できます。 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.「カートに入れる」の流れをもう一度確認する

Web フロント エンド ( http://localhost:5000 ) に戻り、アイテムをカートに追加します。これは、テストとルールがクライアントが必要とする機能と一致していることを確認するための重要なステップです。 (最後に UI を試したとき、ユーザーはアイテムをカートに追加できなかったことを思い出してください!)

69ad26cee520bf24.png

firestore.rulesが保存されると、クライアントは自動的にルールをリロードします。では、カートに何かを追加してみてください。

要約

よくやった!これで、アプリのセキュリティが改善されました。これは、本番環境の準備に不可欠なステップです!これが本番アプリの場合、これらのテストを継続的インテグレーション パイプラインに追加できます。これにより、他の人がルールを変更している場合でも、ショッピング カートのデータにこれらのアクセス制御があるという自信が持てます。

ba5440b193e75967.gif

しかし、待ってください。

続けると、次のことがわかります。

  • Firestore イベントによってトリガーされる関数を作成する方法
  • 複数のエミュレーターで動作するテストを作成する方法

15. Cloud Functions テストをセットアップする

ここまでは、Web アプリのフロントエンドと Firestore セキュリティ ルールに焦点を当ててきました。しかし、このアプリは Cloud Functions を使用してユーザーのカートを最新の状態に保つため、そのコードもテストしたいと考えています。

Emulator Suite を使用すると、Cloud Functions を簡単にテストできます。Cloud Firestore やその他のサービスを使用する関数も同様です。

エディタで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 を見つけることができます。

d6d0429b700d2b21.png

16.Functions テストのウォークスルー

このテストは Cloud Firestore と Cloud Functions の間の相互作用を検証するため、以前の Codelab のテストよりも多くの設定が必要になります。このテストを一通り見て、何が期待されるかを理解しましょう。

カートを作成する

Cloud Functions は信頼できるサーバー環境で実行され、Admin SDK で使用されるサービス アカウント認証を使用できます。最初に、 initializeAdminAppの代わりにinitializeAppを使用してアプリを初期化します。次に、アイテムを追加するカートの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 つのアイテムを追加します。次に、カートに期待されるitemCounttotalPriceがあるかどうかを確認します。もしそうなら、関数はその仕事をしました。

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

新しいターミナル タブを開き (エミュレーターは実行したままにしておきます)、functions ディレクトリに移動します。これは、セキュリティ ルール テストからまだ開いている可能性があります。

$ 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) {
      }
    });

この関数はカート参照を正しく設定していますが、 totalPriceitemCountの値を計算する代わりに、ハードコーディングされた値に更新します。

をフェッチして繰り返します

itemsサブコレクション

新しい定数itemsSnapを初期化し、 itemsのサブコレクションにします。次に、コレクション内のすべてのドキュメントを反復処理します。

// 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 を計算する

まず、 totalPriceitemCountの値をゼロに初期化しましょう。

次に、反復ブロックにロジックを追加します。まず、商品に価格があることを確認します。アイテムに数量が指定されていない場合は、デフォルトで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. Storefront UI を使用して試す

最終テストとして、Web アプリ ( http://localhost:5000/ ) に戻り、アイテムをカートに追加します。

69ad26cee520bf24.png

カートが正しい合計で更新されることを確認します。素晴らしい!

要約

Cloud Functions for Firebase と Cloud Firestore の間の複雑なテスト ケースについて説明しました。テストに合格する Cloud Function を作成しました。また、新機能が UI で動作することも確認しました。これはすべてローカルで行い、自分のマシンでエミュレーターを実行しました。

また、ローカル エミュレーターに対して実行される Web クライアントを作成し、データを保護するようにセキュリティ ルールを調整し、ローカル エミュレーターを使用してセキュリティ ルールをテストしました。

c6a7aeb91fe97a64.gif