Локальная разработка с помощью пакета эмулятора Firebase

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

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

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

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

Для обеспечения возможности локальной разработки вам потребуется подключить приложение к Emulator Suite.

2589e2f95b74fa88.png

Вы также научитесь:

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

2. Настройка

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

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

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

Затем перейдите в каталог 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 CLI

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

$ npm install -g firebase-tools

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

$ firebase --version
9.6.0

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

Создайте проект Firebase.

  1. Войдите в консоль Firebase, используя свою учетную запись Google.
  2. Нажмите кнопку, чтобы создать новый проект, а затем введите название проекта (например, Emulators Codelab ).
  3. Нажмите «Продолжить» .
  4. Если появится запрос, ознакомьтесь с условиями использования Firebase и примите их, после чего нажмите «Продолжить» .
  5. (Необязательно) Включите помощь ИИ в консоли Firebase (в Firebase она называется "Gemini").
  6. Для этого практического занятия вам не понадобится Google Analytics, поэтому отключите эту опцию.
  7. Нажмите «Создать проект» , дождитесь завершения подготовки проекта, а затем нажмите «Продолжить» .

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

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

$ firebase login

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

$ firebase use $YOUR_PROJECT_ID

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

3. Запустите эмуляторы

В этом разделе вы запустите приложение локально. Это значит, что пришло время запустить Emulator Suite.

Запустите эмуляторы

В каталоге с исходным кодом CodeLab выполните следующую команду для запуска эмуляторов:

$ firebase emulators:start --import=./seed

Вы должны увидеть примерно такой вывод:

$ firebase emulators:start --import=./seed
i  emulators: Starting emulators: auth, functions, firestore, hosting
⚠  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: database, pubsub
i  firestore: Importing data from /Users/samstern/Projects/emulators-codelab/codelab-initial-state/seed/firestore_export/firestore_export.overall_export_metadata
i  firestore: Firestore Emulator logging to firestore-debug.log
i  hosting: Serving hosting files from: public
✔  hosting: Local server: http://127.0.0.1:5000
i  ui: Emulator UI logging to ui-debug.log
i  functions: Watching "/Users/samstern/Projects/emulators-codelab/codelab-initial-state/functions" for Cloud Functions...
✔  functions[calculateCart]: firestore function initialized.

┌─────────────────────────────────────────────────────────────┐
│ ✔  All emulators ready! It is now safe to connect your app. │
│ i  View Emulator UI at http://127.0.0.1:4000                │
└─────────────────────────────────────────────────────────────┘

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting        │ 127.0.0.1:5000 │ n/a                             │
└────────────────┴────────────────┴─────────────────────────────────┘
  Emulator Hub running at 127.0.0.1:4400
  Other reserved ports: 4500

Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.

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

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

Судя по таблице в логах, эмулятор Cloud Firestore прослушивает порт 8080 , а эмулятор аутентификации — порт 9099 .

┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator       │ Host:Port      │ View in Emulator UI             │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth      │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions      │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore      │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting        │ 127.0.0.1:5000 │ n/a                             │
└────────────────┴────────────────┴─────────────────────────────────┘

Давайте подключим ваш фронтенд-код к эмулятору, а не к продакшену. Откройте файл public/js/homepage.js и найдите функцию onDocumentReady . Мы видим, что код обращается к стандартным экземплярам Firestore и Auth:

public/js/homepage.js

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

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

public/js/homepage.js

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

  // ADD THESE LINES
  if (location.hostname === "127.0.0.1") {
    console.log("127.0.0.1 detected!");
    auth.useEmulator("http://127.0.0.1:9099");
    db.useEmulator("127.0.0.1", 8080);
  }

Теперь, когда приложение запускается на вашем локальном компьютере (через эмулятор хостинга), клиент Firestore также указывает на локальный эмулятор, а не на производственную базу данных.

Откройте EmulatorUI

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

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

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

4ef88d0148405d36.png

4. Запустите приложение.

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

В веб-браузере перейдите по адресу http://127.0.0.1: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 , когда мы не авторизованы, 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

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

6. Триггеры локальных функций

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

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

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

68c9323f2ad10f7a.png

1) Firestore Write - Клиент

В коллекцию 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 :

functions/index.js

exports.calculateCart = functions.firestore
    .document("carts/{cartId}/items/{itemId}")
    .onWrite(async (change, context) => {
      try {
        let totalPrice = 125.98;
        let itemCount = 8;

        const cartRef = db.collection("carts").doc(context.params.cartId);

        await cartRef.update({
          totalPrice,
          itemCount
        });
      } catch(err) {
      }
    }
);

3) Firestore Write - Admin

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

4) Firestore Read - Клиент

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

public/js/homepage.js

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

Краткий обзор

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

db82eef1706c9058.gif

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

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

7. Создайте правила безопасности, адаптированные под ваше приложение.

Наше веб-приложение читает и записывает данные, но пока мы особо не беспокоились о безопасности. 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;
    }
  }
}

8. Запустите эмуляторы и тесты.

Запустите эмуляторы

В командной строке убедитесь, что вы находитесь в emulators-codelab/codelab-initial-state/ . Возможно, у вас еще запущены эмуляторы с предыдущих шагов. Если нет, запустите эмуляторы заново:

$ firebase emulators:start --import=./seed

После запуска эмуляторов вы можете запускать тесты локально.

Запустите тесты

В командной строке откройте новую вкладку терминала в каталоге emulators-codelab/codelab-initial-state/

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

$ cd functions

Теперь запустите тесты Mocha в каталоге 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

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

9. Безопасный доступ к тележке

Первые две ошибки связаны с проверками "корзины покупок", которые проверяют следующее:

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

функции/тест.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 Authentication , объект request.auth описывает пользователя, выполняющего запрос.

10. Доступ к тестовой тележке

Пакет программ Emulator Suite автоматически обновляет правила при каждом сохранении файла 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

Отличная работа! Теперь у вас есть доступ к корзинам покупок. Переходим к следующему неудачному тесту.

11. Проверьте процесс «Добавить в корзину» в пользовательском интерфейсе.

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

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

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

12. Разрешите доступ к товарам в тележке.

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

  it("can be read only by the cart owner", async () => {
    // Alice can read items in her own cart
    await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart/items/milk").get());

    // Bob can't read items in alice's cart
    await firebase.assertFails(bobDb.doc("carts/alicesCart/items/milk").get())
  });

  it("can be added only by the cart owner",  async () => {
    // Alice can add an item to her own cart
    await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart/items/lemon").set({
      name: "lemon",
      price: 0.99
    }));

    // Bob can't add an item to alice's cart
    await firebase.assertFails(bobDb.doc("carts/alicesCart/items/lemon").set({
      name: "lemon",
      price: 0.99
    }));
  });

Таким образом, мы можем написать правило, разрешающее доступ, если у текущего пользователя тот же UID, что и у владельца (ownerUID) в документе корзины. Поскольку нет необходимости указывать разные правила для create, update, delete , можно использовать правило write , которое применяется ко всем запросам, изменяющим данные.

Обновите правило для документов в подколлекции items. В условном выражении get считывает значение из Firestore — в данном случае, ownerUID документа корзины.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ...

    // UPDATE THESE LINES
    match /carts/{cartID}/items/{itemID} {
      allow read, write: if get(/databases/$(database)/documents/carts/$(cartID)).data.ownerUID == request.auth.uid;
    }

    // ...
  }
}

13. Доступ к элементам тестовой тележки

Теперь мы можем повторно запустить тест. Прокрутите вывод вверх и убедитесь, что другие тесты прошли успешно:

$ npm test

> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha

  shopping carts
    ✓ can be created and updated by the cart owner (195ms)
    ✓ can be read only by the cart owner (136ms)

  shopping cart items
    ✓ can be read only by the cart owner (111ms)
    ✓ can be added only by the cart owner


  adding an item to the cart recalculates the cart total. 
    - should sum the cost of their items


  4 passing (401ms)
  1 pending

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

14. Еще раз проверьте процесс добавления товара в корзину.

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

69ad26cee520bf24.png

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

Краткий обзор

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

ba5440b193e75967.gif

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

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

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

15. Настройка тестов Cloud Functions.

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

Пакет 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.

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

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

d6d0429b700d2b21.png

16. Пошаговое описание функциональных тестов

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

Создать корзину

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

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

Функция корректно устанавливает ссылку на корзину, но вместо вычисления значений 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 нулями.

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

Для заключительного теста вернитесь в веб-приложение ( http://127.0.0.1:5000/ ) и добавьте товар в корзину.

69ad26cee520bf24.png

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

Краткий обзор

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

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

c6a7aeb91fe97a64.gif