Локальная разработка с помощью Firebase Emulator Suite

Бессерверные бэкэнд-инструменты, такие как Cloud Firestore и Cloud Functions, очень просты в использовании, но их сложно протестировать. Пакет локального эмулятора Firebase позволяет запускать локальные версии этих сервисов на вашем компьютере для разработки, чтобы вы могли разрабатывать свое приложение быстро и безопасно.

Предпосылки

  • Простой редактор, например Visual Studio Code, Atom или Sublime Text.
  • Node.js 10.0.0 или выше (для установки Node.js, используйте NVM , чтобы проверить версию, запустить node --version )
  • Java 7 или выше (для установки Java использовать эти инструкции , чтобы проверить версию, запустить java -version )

Что ты будешь делать

В этой лаборатории вы будете запускать и отлаживать простое приложение для онлайн-покупок, которое работает на нескольких сервисах Firebase:

  • Облако Firestore: глобально масштабируемые, бессерверный, NoSQL базы данные с возможностями реального времени.
  • Облако функции: код серверной бессерверной , что работает в ответ на события или HTTP запросов.
  • Firebase Аутентификация: услуга управляемой аутентификации , которая интегрируется с другими продуктами Firebase.
  • Firebase хостинг: быстрый и надежный хостинг для веб - приложений.

Вы подключите приложение к Emulator Suite, чтобы включить локальную разработку.

2589e2f95b74fa88.png

Вы также узнаете, как:

  • Как подключить ваше приложение к Emulator Suite и как подключены различные эмуляторы.
  • Как работают правила безопасности Firebase и как протестировать правила безопасности Firestore на локальном эмуляторе.
  • Как написать функцию Firebase, которая запускается событиями Firestore, и как написать интеграционные тесты, которые работают с Emulator Suite.

Получите исходный код

В этой кодовой лаборатории вы начинаете с почти завершенной версии образца Fire Store, поэтому первое, что вам нужно сделать, это клонировать исходный код:

$ git clone https://github.com/firebase/emulators-codelab.git

Затем перейдите в каталог codelab, где вы будете работать над оставшейся частью этой codelab:

$ cd emulators-codelab/codelab-initial-state

Теперь установите зависимости, чтобы можно было запустить код. Если у вас более медленное интернет-соединение, это может занять минуту или две:

# Move into the functions directory
$ cd functions

# Install dependencies
$ npm install

# Move back into the previous directory
$ cd ../

Получить интерфейс командной строки Firebase

Emulator Suite является частью Firebase CLI (интерфейса командной строки), который можно установить на вашем компьютере с помощью следующей команды:

$ npm install -g firebase-tools

Затем убедитесь, что у вас установлена ​​последняя версия интерфейса командной строки. Эта кодовая лаборатория должна работать с версией 9.0.0 или выше, но более поздние версии содержат больше исправлений ошибок.

$ firebase --version
9.6.0

Подключитесь к своему проекту Firebase

Если у Вас нет проекта Firebase, в Firebase консоли , создать новый проект Firebase. Запишите выбранный вами идентификатор проекта, он понадобится вам позже.

Теперь нам нужно подключить этот код к вашему проекту Firebase. Сначала выполните следующую команду, чтобы войти в интерфейс командной строки Firebase:

$ firebase login

Затем выполните следующую команду, чтобы создать псевдоним проекта. Заменить $YOUR_PROJECT_ID с идентификатором вашего проекта Firebase.

$ firebase use $YOUR_PROJECT_ID

Теперь вы готовы запустить приложение!

В этом разделе вы запустите приложение локально. Это означает, что пора загрузить 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.

После того, как вы видите все эмуляторы , созданные сообщение, приложение готово к использованию.

Подключите веб-приложение к эмуляторам

На основании таблицы в журналах мы можем видеть , что эмулятор Cloud 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:

общедоступный / js / homepage.js

  const auth = firebaseApp.auth();
  const db = firebaseApp.firestore();

Давайте обновим db и auth объектов , чтобы указать на местных эмуляторы:

общедоступный / 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

В вашем веб - браузере, перейдите к HTTP: // локальный: 4000 / . Вы должны увидеть пользовательский интерфейс Emulator Suite.

Домашний экран пользовательского интерфейса эмуляторов

Щелкните, чтобы увидеть пользовательский интерфейс для эмулятора Firestore. items коллекции уже содержит данные из - за данных , импортируемых с --import флагом.

4ef88d0148405d36.png

Откройте приложение

В вашем веб - браузере, перейдите к HTTP: // локальный: 5000 , и вы должны увидеть Огонь магазин работает локально на вашем компьютере!

939f87946bac2ee4.png

Использовать приложение

Выберите элемент на главной странице и нажмите кнопку Добавить в корзину. К сожалению, вы столкнетесь со следующей ошибкой:

a11bd59933a8e885.png

Давайте исправим эту ошибку! Поскольку все работает в эмуляторах, мы можем экспериментировать и не беспокоиться о влиянии на реальные данные.

Найдите ошибку

Хорошо, заглянем в консоль разработчика Chrome. Нажмите Control+Shift+J (Windows, Linux, Chrome OS) или Command+Option+J (Mac) , чтобы увидеть ошибку на консоли:

74c45df55291dab1.png

Похоже , что там была какая - то ошибка в addToCart метода, давайте посмотрим на это. Где мы стараемся доступа , что называется uid в этом методе , и почему это было бы null ? Прямо сейчас метод выглядит в public/js/homepage.js :

общедоступный / 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 . Добавим для этого проверку:

общедоступный / 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

Не волнуйтесь, мы скоро исправим эту ошибку. Во-первых, давайте углубимся в то, что на самом деле произошло, когда вы добавили товар в корзину.

Нажатие Добавить в корзину пинает цепи событий , которые включают несколько эмуляторов. В журналах интерфейса командной строки Firebase после добавления товара в корзину вы должны увидеть что-то вроде следующих сообщений:

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

Для создания этих журналов и наблюдаемого вами обновления пользовательского интерфейса произошло четыре ключевых события:

68c9323f2ad10f7a.png

1) Запись в Firestore - Клиент

Новый документ добавляется в коллекцию Firestore /carts/{cartId}/items/{itemId}/ . Вы можете увидеть этот код в addToCart функции внутри public/js/homepage.js :

общедоступный / 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) Запись в Firestore - Администратор

calculateCart функция считывает все элементы в корзине и складывает общее количество и цену, то он обновляет «корзину» документ с новыми итогами (см cartRef.update(...) выше).

4) Чтение из Firestore - Клиент

Веб-интерфейс подписан на получение обновлений об изменениях в корзине. Он получает обновления в режиме реального времени после того , как функция Облака записывает новые итоги и обновляет интерфейс, как вы можете видеть в public/js/homepage.js :

общедоступный / js / homepage.js

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

Резюме

Хорошая работа! Вы просто настроили полностью локальное приложение, которое использует три разных эмулятора Firebase для полностью локального тестирования.

db82eef1706c9058.gif

Но подождите, это еще не все! В следующем разделе вы узнаете:

  • Как писать модульные тесты, использующие эмуляторы Firebase.
  • Как использовать эмуляторы Firebase для отладки ваших правил безопасности.

Наше веб-приложение читает и записывает данные, но пока мы совсем не беспокоимся о безопасности. Cloud Firestore использует систему под названием «Правила безопасности», чтобы объявить, кто имеет доступ для чтения и записи данных. Emulator Suite - отличный способ прототипировать эти правила.

В редакторе откройте файл emulators-codelab/codelab-initial-state/firestore.rules . Вы увидите, что в наших правилах есть три основных раздела:

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 :

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/

Сначала перейдите в каталог функций (мы останемся здесь до конца кодовой лаборатории):

$ cd functions

Теперь запустите тесты мокко в каталоге функций и прокрутите до начала вывода:

# 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

Сейчас у нас четыре неудачи. По мере создания файла правил вы можете измерять прогресс, наблюдая за прохождением большего количества тестов.

Первые две ошибки - это тесты «корзины покупок», которые проверяют, что:

  • Пользователи могут создавать и обновлять только свои собственные корзины.
  • Пользователи могут читать только свои тележки

функции / 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;
    }

    // ...
  }
}

Эти правила теперь разрешают доступ для чтения и записи только владельцу корзины.

Для проверки входящих данных и аутентификации пользователя мы используем два объекта, которые доступны в контексте каждого правила:

  • request объект содержит данные и метаданные об операции , которая в настоящее время попытки.
  • Если проект Firebase использует Firebase аутентификацию , то request.auth объект описывает пользователь , который делает запрос.

Эмулятор Люкс автоматически обновляет правила всякий раз , когда firestore.rules сохраняется. Вы можете подтвердить , что эмулятор обновленных правил, глядя на вкладке работает эмулятор для сообщений Rules updated :

5680da418b420226.png

Перезапустите тесты и убедитесь, что первые два теста проходят:

$ 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

Молодец! Теперь у вас есть безопасный доступ к тележкам для покупок. Перейдем к следующему провальному тесту.

В настоящий момент, хотя владельцы тележек читают и пишут в свою корзину, они не могут читать или записывать отдельные товары в своей тележке. Это потому , что в то время как владельцы имеют доступ к документу корзины, они не имеют доступ к телеге по пунктам подколлекции.

Это неработающее состояние для пользователей.

Вернуться к веб - интерфейс, который работает на http://localhost:5000, и попытаться что- то добавить в корзину. Вы получаете Permission Denied ошибку, видимую из консоли отладки, потому что мы еще не получили доступ пользователей к создаваемым документам в items подколлекции.

Эти два теста подтверждают, что пользователи могут добавлять товары или читать товары только из своей корзины:

  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 правило, которое распространяется на все запросы, изменяющие данные.

Обновите правило для документов в подколлекции товаров. 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

Отлично! Теперь все наши тесты пройдены. У нас есть один незавершенный тест, но мы доберемся до него за несколько шагов.

Возврат к веб переднего конца ( HTTP: // локальный: 5000 ) и добавьте товар в корзину. Это важный шаг для подтверждения того, что наши тесты и правила соответствуют функциональности, требуемой клиентом. (Помните, что в последний раз, когда мы опробовали пользовательский интерфейс, пользователи не могли добавлять товары в свою корзину!)

69ad26cee520bf24.png

Клиент автоматически перезагружает правила , когда firestore.rules сохраняется. Итак, попробуйте добавить что-нибудь в корзину.

Резюме

Хорошая работа! Вы только что повысили безопасность своего приложения, что является важным шагом на пути к его подготовке к работе! Если бы это было производственное приложение, мы могли бы добавить эти тесты в наш конвейер непрерывной интеграции. Это вселит в нас уверенность в том, что данные нашей корзины покупок будут иметь эти элементы управления доступом, даже если другие изменяют правила.

ba5440b193e75967.gif

Но подождите, это еще не все!

если вы продолжите, вы узнаете:

  • Как написать функцию, запускаемую событием Firestore
  • Как создавать тесты, которые работают с несколькими эмуляторами

Пока что мы сосредоточились на интерфейсе нашего веб-приложения и правилах безопасности Firestore. Но это приложение также использует облачные функции, чтобы поддерживать корзину пользователя в актуальном состоянии, поэтому мы также хотим протестировать этот код.

Emulator Suite упрощает тестирование облачных функций, даже функций, использующих 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 Project ID .:

// CHANGE THIS LINE
const REAL_FIREBASE_PROJECT_ID = "changeme";

Если вы забыли свой идентификатор проекта, вы можете найти свой идентификатор проекта Firebase в настройках проекта в консоли Firebase:

d6d0429b700d2b21.png

Поскольку этот тест проверяет взаимодействие между Cloud Firestore и 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 подколлекции нашей корзины документа для того , чтобы вызвать функцию. Добавьте два элемента, чтобы убедиться, что вы тестируете добавление, которое происходит в функции.

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 доллара. Затем проверьте , если тележка имеет ожидаемый 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();
        };
      });
    });
   });
 });

Возможно, у вас еще работают эмуляторы из предыдущих тестов. Если нет, запустите эмуляторы. Из командной строки запустите

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

Функция правильно установки корзин ссылки, но вместо того чтобы вычислять значения totalPrice и itemCount , он обновляет их закодированные из них.

Получить и перебрать

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

Во- первых, давайте инициализировать значения totalPrice и itemCount к нулю.

Затем добавьте логику в наш блок итерации. Сначала убедитесь, что у товара есть цена. Если элемент не уточнило количество, пусть он по умолчанию 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)

Молодец!

Для окончательного теста, возвращение к веб - приложение ( HTTP: // локальный: 5000 / ) и добавьте товар в корзину.

69ad26cee520bf24.png

Убедитесь, что корзина обновляется с правильной суммой. Фантастика!

Резюме

Вы прошли сложный тестовый пример между Cloud Functions for Firebase и Cloud Firestore. Вы написали облачную функцию, чтобы пройти тест. Вы также подтвердили, что новая функциональность работает в пользовательском интерфейсе! Вы сделали все это локально, запустив эмуляторы на своей машине.

Вы также создали веб-клиент, работающий с локальными эмуляторами, настроили правила безопасности для защиты данных и протестировали правила безопасности с помощью локальных эмуляторов.

c6a7aeb91fe97a64.gif