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

1.始める前に

CloudFirestoreやCloudFunctionsなどのサーバーレスバックエンドツールは非常に使いやすいですが、テストが難しい場合があります。 Firebase Local Emulator Suiteを使用すると、開発マシンでこれらのサービスのローカルバージョンを実行できるため、アプリをすばやく安全に開発できます。

前提条件

  • Visual Studio Code、Atom、SublimeTextなどのシンプルなエディター
  • Node.js 10.0.0以降(Node.jsをインストールするには、 nvmを使用し、バージョンを確認するには、 node --versionを実行します)
  • Java 7以降(Javaをインストールするには、これらの手順を使用し、バージョンを確認するには、 java -versionを実行します)

あなたがすること

このコードラボでは、複数のFirebaseサービスを利用したシンプルなオンラインショッピングアプリを実行してデバッグします。

  • Cloud Firestore:リアルタイム機能を備えたグローバルにスケーラブルなサーバーレスのNoSQLデータベース。
  • Cloud Functions :イベントまたはHTTPリクエストに応答して実行されるサーバーレスバックエンドコード。
  • Firebase認証:他のFirebase製品と統合するマネージド認証サービス。
  • Firebase Hosting :ウェブアプリの高速で安全なホスティング。

アプリをエミュレータースイートに接続して、ローカル開発を有効にします。

2589e2f95b74fa88.png

また、次の方法も学習します。

  • アプリをエミュレータースイートに接続する方法と、さまざまなエミュレーターを接続する方法。
  • Firebaseセキュリティルールの仕組みと、ローカルエミュレータに対してFirestoreセキュリティルールをテストする方法。
  • FirestoreイベントによってトリガーされるFirebase関数を作成する方法と、EmulatorSuiteに対して実行される統合テストを作成する方法。

2.セットアップ

ソースコードを入手する

このコードラボでは、ほぼ完成しているバージョンの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 ../

FirebaseCLIを入手する

Emulator Suiteは、Firebase CLI(コマンドラインインターフェイス)の一部であり、次のコマンドを使用してマシンにインストールできます。

$ npm install -g firebase-tools

次に、CLIの最新バージョンを使用していることを確認します。このコードラボはバージョン9.0.0以降で動作するはずですが、それ以降のバージョンにはさらに多くのバグ修正が含まれています。

$ firebase --version
9.6.0

Firebaseプロジェクトに接続します

Firebaseプロジェクトがない場合は、 Firebaseコンソールで新しいFirebaseプロジェクトを作成します。選択したプロジェクトIDをメモしておきます。後で必要になります。

次に、このコードをFirebaseプロジェクトに接続する必要があります。まず、次のコマンドを実行してFirebaseCLIにログインします。

$ firebase login

次に、次のコマンドを実行してプロジェクトエイリアスを作成します。 $YOUR_PROJECT_IDをFirebaseプロジェクトのIDに置き換えます。

$ firebase use $YOUR_PROJECT_ID

これで、アプリを実行する準備が整いました。

3.エミュレーターを実行します

このセクションでは、アプリをローカルで実行します。これは、EmulatorSuiteを起動するときが来たことを意味します。

エミュレータを起動します

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.

すべてのエミュレーターが開始されたというメッセージが表示されたら、アプリを使用する準備ができています。

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

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

EmulatorUIを開きます

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

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

クリックして、FirestoreEmulatorのUIを表示します。 --importフラグでインポートされたデータのため、 itemsコレクションにはすでにデータが含まれています。

4ef88d0148405d36.png

4.アプリを実行します

アプリを開く

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

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のドキュメントによると、ログインしていない場合、 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.ローカル関数トリガー

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

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

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

68c9323f2ad10f7a.png

1)FirestoreWrite-クライアント

新しいドキュメントが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トリガーを使用して、カートアイテムに発生する書き込みイベント(作成、更新、または削除)をリッスンします。

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)FirestoreWrite-管理者

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

4)FirestoreRead-クライアント

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エミュレータを使用する単体テストの作成方法。
  • 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;
    }
  }
}

今、誰でも私たちのデータベースにデータを読み書きできます!有効な操作のみが実行され、機密情報が漏洩しないようにする必要があります。

このコードラボでは、最小特権の原則に従って、すべてのドキュメントをロックダウンし、すべてのユーザーが必要なすべてのアクセス権を取得するまで徐々にアクセス権を追加しますが、それ以上は追加しません。条件を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プロジェクトがFirebaseAuthenticationを使用している場合、 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の「カートに追加」フローを確認します

現在、カートの所有者はカートの読み取りと書き込みを行っていますが、カート内の個々のアイテムの読み取りと書き込みを行うことはできません。これは、所有者はカートドキュメントにアクセスできますが、カートのアイテムサブコレクションにはアクセスできないためです。

これはユーザーにとって壊れた状態です。

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ルールを使用できます。

アイテムサブコレクション内のドキュメントのルールを更新します。条件付きの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.CloudFunctionsテストを設定します

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

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

エディターで、 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.機能テストをウォークスルーします

このテストはCloudFirestoreとCloudFunctions間の相互作用を検証するため、以前のコードラボのテストよりも多くのセットアップが必要です。このテストをウォークスルーして、それが何を期待するかを理解しましょう。

カートを作成する

Cloud Functionsは信頼できるサーバー環境で実行され、AdminSDKで使用されるサービスアカウント認証を使用できます。まず、 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.ストアフロントUIを使用して試してみてください

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

69ad26cee520bf24.png

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

要約

Cloud FunctionsforFirebaseとCloudFirestoreの間の複雑なテストケースについて説明しました。テストに合格するためのクラウド関数を作成しました。また、新しい機能がUIで機能していることも確認しました。これらすべてをローカルで実行し、自分のマシンでエミュレーターを実行しました。

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

c6a7aeb91fe97a64.gif