Google は、黒人コミュニティのための人種的公平の促進に取り組んでいます。詳細をご覧ください。

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

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サービスを利用したシンプルなオンラインショッピングアプリを実行してデバッグします。

  • クラウドFirestore:リアルタイム機能を持つグローバルスケーラブル、サーバレス、NoSQLのデータベース。
  • クラウド機能:サーバレスバックエンドのコードイベントまたはHTTPリクエストに応じて実行しています。
  • Firebase認証:他のFirebase製品との統合管理認証サービス。
  • 高速でのWebアプリケーションのホスティング確保:Firebaseホスティング

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

2589e2f95b74fa88.png

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

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

ソースコードを入手する

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

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

このセクションでは、アプリをローカルで実行します。これは、エミュレータスイートを起動するときが来たことを意味します。

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

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アプリをエミュレーターに接続します

ログ内のテーブルに基づいて、我々はクラウドFirestoreエミュレータがポートで待機していることがわかります8080と認証エミュレータはポートで待機している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();

レッツ・更新dbauth地元のエミュレータを指すように、オブジェクト:

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 / 。 Emulator SuiteUIが表示されます。

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

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

4ef88d0148405d36.png

アプリを開く

Webブラウザで、に移動します。http:// localhostを:5000 、あなたは火ストアがあなたのマシン上でローカルに実行されている表示されるはずです!

939f87946bac2ee4.png

アプリを使用する

ホームページ上の項目を選び、カートに追加]をクリックします。残念ながら、次のエラーが発生します。

a11bd59933a8e885.png

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

バグを見つける

では、Chrome開発者コンソールを見てみましょう。プレスControl+Shift+J (WindowsやLinux、クローム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認証のドキュメント我々はサインインしていないとき、 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;
    }

    // ...
  }

アプリをテストする

さて、ページを更新してからカートに追加]をクリックします。今回はより良いエラーが発生するはずです:

c65f6c05588133f7.png

あなたは上部のツールバーにある[サインイン]クリックしますし、再度カートに追加]をクリックした場合しかし、あなたはカートが更新されていることがわかります。

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

239f26f02f959eef.png

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

複数のエミュレータを伴う一連の出来事オフカートキックに追加]をクリック。カートにアイテムを追加すると、FirebaseCLIログに次のようなメッセージが表示されます。

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

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

68c9323f2ad10f7a.png

1)FirestoreWrite-クライアント

新しい文書はFirestoreコレクションに追加され/carts/{cartId}/items/{itemId}/ 。あなたはこのコードを見ることができaddToCart内部の機能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);
  }

2)クラウド機能がトリガーされました

クラウド機能calculateCart使用により、カートのアイテムに起こるすべての書き込みイベント(作成、更新、または削除)をリッスンonWriteあなたが見ることができますトリガ、 functions/index.js

関数/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フロントエンドは、カートの変更に関する最新情報を受け取るようにサブスクライブされています。クラウド機能は、新しい合計を書き込み、あなたが見ることができるように、UIを更新した後、それはリアルタイムの更新を取得しpublic/js/homepage.js

public / js / homepage.js

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

要約

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

db82eef1706c9058.gif

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

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

私たちの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;
    }
  }
}

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

このコードラボでは、最小特権の原則に従って、すべてのドキュメントをロックダウンし、すべてのユーザーが必要なすべてのアクセス権を取得できるようになるまで、徐々にアクセス権を追加しますが、それ以上は追加しません。レッツ・アップデート最初の2つのルールをに条件を設定することで、アクセスを拒否するためにfalse

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

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

コマンドラインで、あなたがにしている作る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つの失敗があります。ルールファイルを作成するときに、さらに多くのテストに合格するのを監視することで、進捗状況を測定できます。

最初の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認証をrequest.authオブジェクトは、要求を行っているユーザーを説明しています。

いつでもエミュレータ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

よくできた!これで、ショッピングカートへのアクセスが保護されました。次の失敗したテストに移りましょう。

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

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

上で実行されているウェブUIに戻りhttp://localhost:5000,およびショッピングカートに何かを追加してみてください。あなたは取得Permission Denied我々はまだで作成された文書へのユーザーアクセスを許可していないので、デバッグコンソールから見える誤差を、 itemsサブコレクション。

これらの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;
    }

    // ...
  }
}

これで、テストを再実行できます。出力の一番上までスクロールし、さらにテストに合格することを確認します。

$ 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つありますが、いくつかの手順でそれを実現します。

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

69ad26cee520bf24.png

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

要約

よくやった!アプリのセキュリティを向上させました。これは、アプリを本番環境に移行するための重要なステップです。これが本番アプリの場合、継続的インテグレーションパイプラインにこれ​​らのテストを追加できます。これにより、他の人がルールを変更している場合でも、ショッピングカートデータにこれらのアクセス制御が含まれるという自信が得られます。

ba5440b193e75967.gif

しかし、待ってください、もっとあります!

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

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

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

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

エディタでは、オープン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

このテストは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();
        };
      });
    });
   });
 });

以前のテストからエミュレーターを実行している可能性があります。そうでない場合は、エミュレーターを起動します。コマンドラインから、を実行します

$ 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

特定の障害を見ると、タイムアウトエラーのように見えます。これは、テストが関数が正しく更新されるのを待っているが、更新されないためです。これで、テストを満たす関数を作成する準備が整いました。

このテストを修正するには、中に機能を更新する必要がある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);
      }
    });

コマンドラインで、エミュレーターがまだ実行中であることを確認し、テストを再実行します。エミュレーターは機能への変更を自動的に取得するため、エミュレーターを再起動する必要はありません。すべてのテストに合格するはずです。

$ 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)

よくできた!

最終テストのために、Webアプリケーションへのリターン(のhttp:// localhostを:5000 / )と、カートにアイテムを追加します。

69ad26cee520bf24.png

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

要約

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

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

c6a7aeb91fe97a64.gif